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...
Loading...
Install Wheels and get a local development server running
By far the quickest way to get started with Wheels is via CommandBox. 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 CommandBox 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 Chocolatey on Windows, Homebrew 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.
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, 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!
Manage environment-specific configuration for your Wheels application.
wheels config env <action> [source] [target]
action
- (Required) Action to perform: list
, create
, copy
source
- (Optional) Source environment for copy action
target
- (Optional) Target environment for create or copy action
The wheels config env
command provides tools for managing environment-specific configurations. It helps you list, create, and copy configurations between different environments.
wheels config env list
wheels config env create production
wheels config env copy development production
Display all available environments:
wheels config env list
Output:
Available Environments:
----------------------
• development (active)
• testing
• maintenance
• production
Create a new environment configuration:
wheels config env create staging
This creates a new environment configuration file at /config/staging/settings.cfm
.
Copy configuration from one environment to another:
wheels config env copy development staging
This copies all settings from the development environment to the staging environment, preserving environment-specific values like datasource names.
Some operations require application restart
Sensitive values protected by default
Changes logged for audit purposes
Use templates for consistency
wheels config list - List all settings
wheels config set - Set configuration values
wheels env - Environment management
With CommandBox, we don't need to have Lucee or Adobe ColdFusion installed locally. With a simple command, we can make CommandBox go and get the CFML engine we've requested, and quickly create a server running on Undertow. Make sure you're in the root of your website, and then run:
server start
The server will then start on a random port on 127.0.0.1
based the configuration from the server.json
file that is in the root of your application that comes with wheels. We can add various options to server.json
to customize our server. Your default server.json
will look something like this:
In this server.json
file, the server name is set to wheels
, meaning I can now start the server from any directory by simply calling start myApp
. We don't have any port specified, but you can specify any port you want. Lastly, we have URL rewriting enabled and pointed the URL rewrite configuration file to public/urlrewrite.xml
, which is included starting from Wheels 2.x.
You can also specify hosts other than localhost: there's a useful CommandBox module to do that () which will automatically create entries in your hosts file to allow for domains such as myapp.local
running on port 80. You can install it via install commandbox-hostupdater
when running the box shell with administrator privileges.
Obviously, anything you start, you might want to stop. Servers can be stopped either via right/ctrl clicking on the icon in the taskbar, or by the stop
command. To stop a server running in the current directory issue the following:
server stop
You can also stop a server from anywhere by using its name:
server stop myApp
If you want to see what server configurations exist on your system and their current status, simply do server list
server list
To remove a server configuration from the list, you can use server forget myapp
. Note the status of the servers on the list are somewhat unreliable, as it only remembers the last known state of the server: so if you start a server and then turn off your local machine, it may still remember it as running
when you turn your local machine back on, which is why we recommend the use of force: true
in the server.json
file.
By default, CommandBox will run Lucee (version 6.x at time of writing). You may wish to specify an exact version of Lucee, or use Adobe ColdFusion. We can do this via either setting the appropriate cfengine
setting in server.json
, or at runtime with the cfengine=
argument.
Start the default engine
CommandBox> start
__
Start the latest stable Lucee 5.x engine
CommandBox> start cfengine=lucee5
__
Start a specific engine and version
CommandBox> start [email protected]
__
Start the most recent Adobe server that starts with version "11"
CommandBox> start cfengine=adobe@11
__
Start the most recent adobe engine that matches the range
CommandBox> start cfengine="adobe@>9.0 <=11"
Or via server.json
You can of course run multiple servers, so if you need to test your app on Lucee 5.x, Lucee 6.x and Adobe 2018, you can just start three servers with different cfengine=
arguments!
By default, the Lucee server that CommandBox starts includes all the essential Lucee extensions you need, but if need to minimize the size of the Lucee instance you launch, then you can use Lucee-Light by specifying cfengine=lucee-light
in your server.json
file. Wheels can run just fine on lucee-light (which is after all, Lucee, minus all the extensions) but at a minimum, requires the following extensions to be installed as dependencies in your box.json
. Please note you may have to add any drivers you need for your database to this list as well.
Once added to your box.json file, while the server is running, just do box install
, which will install the dependencies, and load them into the running server within 60 seconds.
Alternatively you can download the extensions and add them manually to your server root's deploy folder (i.e \WEB-INF\lucee-server\deploy
)
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
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. The datasource must already exist in your CFML server configuration - this command creates the database itself, not the datasource.
Specify which datasource to use. If not provided, uses the default datasource from your Wheels configuration.
Specify the environment to use. Defaults to the current environment.
Create database using default datasource:
Create database for development:
Create database for testing:
Creates database with UTF8MB4 character set
Uses utf8mb4_unicode_ci collation
Connects to information_schema
to execute CREATE DATABASE
Creates database with UTF8 encoding
Checks if database already exists before creating
Connects to postgres
system database
Creates database with default settings
Checks if database already exists before creating
Connects to master
system database
Displays message that H2 databases are created automatically
No action needed - database file is created on first connection
Datasource Configuration: The datasource must be configured in your CFML server admin
Database Privileges: The database user must have CREATE DATABASE privileges
Network Access: The database server must be accessible
The specified datasource doesn't exist in your server configuration. Create it in your CFML admin first.
The database already exists. Use wheels db drop
first if you need to recreate it.
The database user doesn't have permission to create databases. Grant CREATE privileges to the user.
- Drop an existing database
- Create and setup database
- Run migrations after creating database
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.
Specify which datasource's database to drop. If not provided, uses the default datasource from your Wheels configuration.
Specify the environment to use. Defaults to the current environment.
Skip the confirmation prompt. Useful for scripting.
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
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
Deletes database files (.mv.db, .lock.db, .trace.db)
Shows which files were deleted
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.
- Create a new database
- Drop and recreate database
- Backup before dropping
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
Set configuration values for your Wheels application.
setting
- (Required) Key=Value pair for the setting to update
--environment
- (Optional) Environment to apply settings to: development
, testing
, production
, all
. Default: development
--encrypt
- (Optional) Encrypt sensitive values
The wheels config set
command updates configuration settings in your Wheels application. Settings must be provided in key=value
format.
The command accepts various value types:
Settings are saved to environment-specific configuration files:
Development: /config/development/settings.cfm
Testing: /config/testing/settings.cfm
Production: /config/production/settings.cfm
All environments: /config/settings.cfm
Example:
Use the --encrypt
flag for sensitive values:
Target specific environments with the --environment
flag:
Use environment-specific settings: Don't set production values in development
Encrypt sensitive data: Use --encrypt
for passwords and keys
Test changes: Verify settings with wheels config list
Restart after changes: Some settings require application restart
Some settings require application restart
Encrypted values can't be read back
Changes are logged for audit
Use environment variables for containers
- List configuration
- Environment config
- Environment management
Restart the Wheels development server and reload the application.
The wheels server restart
command restarts a running CommandBox server and automatically reloads the Wheels application. This ensures that both server-level and application-level changes are picked up.
name
Type: String
Description: Name of the server to restart
Example: wheels server restart name=myapp
--force
Type: Boolean flag
Description: Force restart even if server appears stopped
Example: wheels server restart --force
Stops the running server
Starts the server again with the same configuration
Automatically runs wheels reload
to refresh the application
Confirms successful restart
This command is particularly useful when you've made changes to server configuration or installed new dependencies
The automatic application reload ensures that Wheels picks up any code changes
If the reload fails, you may need to manually refresh your browser
- Start the server
- Stop the server
- Check server status
- Reload just the application
{
"name":"wheels",
"web":{
"host":"localhost",
"webroot":"public",
"rewrites":{
"enable":true,
"config":"public/urlrewrite.xml"
}
},
"app":{
"cfengine":"lucee"
}
}
myapp (stopped)
http://127.0.0.1:60000
Webroot: /Users/wheels/Documents/myapp
myAPI (stopped)
http://127.0.0.1:60010
Webroot: /Users/wheels/Documents/myAPI
megasite (stopped)
http://127.0.0.1:61280
CF Engine: lucee 4.5.4+017
Webroot: /Users/wheels/Documents/megasite
awesomesite (stopped)
http://127.0.0.1:60015
CF Engine: lucee 4.5.4+017
Webroot: /Users/wheels/Documents/awesomeo
{
"name":"myApp",
"force":true,
"web":{
"http":{
"host":"localhost",
"port":60000
},
"rewrites":{
"enable":true,
"config":"urlrewrite.xml"
}
},
"app":{
"cfengine":"adobe2018"
},
}
"dependencies":{
"lucee-image":"lex:https://ext.lucee.org/lucee.image.extension-1.0.0.35.lex",
"lucee-zip": "lex:https://ext.lucee.org/compress-extension-1.0.0.2.lex",
"lucee-esapi": "lex:https://ext.lucee.org/esapi-extension-2.1.0.18.lex"
}
wheels info
--help
Show help information
,--. ,--.,--. ,--. ,-----.,--. ,--.
| | | || ,---. ,---. ,---. | | ,---. ' .--./| | | |
| |.'.| || .-. || .-. :| .-. :| |( .-' | | | | | |
| ,'. || | | |\ --.\ --.| |.-' `) ' '--'\| '--.| |
'--' '--'`--' `--' `----' `----'`--'`----' `-----'`-----'`--'
============================ Wheels CLI ============================
Current Working Directory: /Users/username/myapp
CommandBox Module Root: /Users/username/.CommandBox/cfml/modules/cfwheels-cli/
Current Wheels Version in this directory: 2.5.0
====================================================================
wheels db create [--datasource=<name>] [--environment=<env>]
wheels db create --datasource=myapp_dev
wheels db create --environment=testing
wheels db create
wheels db create --datasource=myapp_dev
wheels db create --datasource=myapp_test --environment=testing
wheels db drop [--datasource=<name>] [--environment=<env>] [--force]
wheels db drop --datasource=myapp_dev
wheels db drop --environment=testing
wheels db drop --force
wheels db drop
# Will prompt: Are you sure you want to drop the database 'myapp_dev'? Type 'yes' to confirm:
wheels db drop --force
wheels db drop --datasource=myapp_test --environment=testing --force
wheels db dump --output=backup-before-drop.sql
wheels db drop
wheels dbmigrate up
wheels 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 info
wheels config set <key>=<value> [--environment=<env>] [--encrypt]
wheels config set dataSourceName=wheels_production
wheels config set showDebugInformation=false --environment=production
wheels config set reloadPassword=newPassword --environment=production
wheels config set defaultLayout=main --environment=all
wheels config set apiKey=sk_live_abc123 --encrypt
wheels config set dataSourcePassword=mySecret --encrypt
wheels config set appName="My Wheels App"
wheels config set [email protected]
wheels config set showDebugInformation=true
wheels config set cacheQueries=false
wheels config set sessionTimeout=1800
wheels config set maxUploadSize=10485760
// Added to /config/production/settings.cfm
set(dataSourceName="wheels_production");
wheels config set reloadPassword=mySecret --encrypt
wheels config set apiKey=sk_live_123456 --encrypt
# Development only
wheels config set showDebugInformation=true --environment=development
# Production only
wheels config set cacheQueries=true --environment=production
# All environments
wheels config set appName="My App" --environment=all
wheels server restart [options]
# Restart the default server
wheels server restart
# Restart a specific named server
wheels server restart name=myapp
# Force restart
wheels server restart --force
Bootstrap an existing Wheels application for CLI usage.
wheels init
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.
--help
Show help 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)
wheels init
Example interaction:
==================================== 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
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
{
"name": "myapp",
"web": {
"http": {
"port": 60000
}
},
"app": {
"cfengine": "lucee5"
}
}
{
"name": "myapp",
"version": "1.0.0",
"dependencies": {
"wheels": "^2.5.0"
}
}
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
wheels generate app - Create a new Wheels application
wheels reload - Reload the application
wheels info - Display version information
Reset the database by dropping it and recreating it from scratch.
wheels db reset [--datasource=<name>] [--environment=<env>] [--force] [--skip-seed] [--seed-count=<n>]
The wheels db reset
command completely rebuilds your database by executing four operations in sequence:
Drops the existing database (wheels db drop
)
Creates a new database (wheels db create
)
Runs all migrations (wheels dbmigrate latest
)
Seeds the database with sample data (wheels db seed
)
This is a destructive operation that will delete all existing data.
Specify which datasource to use. If not provided, uses the default datasource from your Wheels configuration.
wheels db reset --datasource=myapp_dev
Specify the environment to use. Defaults to the current environment.
wheels db reset --environment=testing
Skip the confirmation prompt. Use with caution!
wheels db reset --force
Skip the database seeding step.
wheels db reset --skip-seed
Number of records to generate per model when seeding. Defaults to 5.
wheels db reset --seed-count=20
Reset with confirmation:
wheels db reset
# Will prompt: Are you sure you want to reset the database? Type 'yes' to confirm:
Reset without confirmation:
wheels db reset --force
wheels db reset --datasource=myapp_test --environment=testing --force
wheels db reset --skip-seed --force
Confirmation Required: Must type "yes" to confirm (unless --force)
Production Warning: Extra warning for production environment
Special Production Confirmation: Must type "reset production database" for production
This operation is irreversible! All data will be permanently lost.
# When you need a fresh start
wheels db reset --force --seed-count=50
# Backup first
wheels db dump --output=backup-before-reset.sql
# Then reset
wheels db reset
# In test scripts
wheels db reset --environment=testing --force --skip-seed
Drop Database
All tables and data are deleted
Database is completely removed
Create Database
Fresh database is created
Character set and collation are set (MySQL)
Run Migrations
All migrations run from scratch
Schema is recreated
Seed Database (unless --skip-seed)
Sample data is generated
Useful for development/testing
If reset fails partway through:
# Manual recovery steps
wheels db drop --force # Ensure old database is gone
wheels db create # Create new database
wheels dbmigrate latest # Run migrations
wheels db seed # Seed data (optional)
Always backup production data first
Use --force only in automated scripts
Avoid resetting production databases
Use db setup for new databases instead
wheels db setup
- Setup new database (non-destructive)
wheels db drop
- Just drop database
wheels db dump
- Backup before reset
wheels dbmigrate reset
- Reset just migrations
Manage the Wheels development server with enhanced functionality.
wheels server [subcommand] [options]
The wheels server
command provides a suite of server management tools that wrap CommandBox's native server functionality with Wheels-specific enhancements. These commands add validation, helpful error messages, and integration with the Wheels framework.
start
- Start the development server
stop
- Stop the development server
restart
- Restart the development server
status
- Show server status
log
- Tail server logs
open
- Open application in browser
All server commands check that you're working in a valid Wheels application directory before executing. This prevents common errors and provides helpful guidance.
When checking server status, the commands also display:
Wheels framework version
Application root directory
Quick action suggestions
The restart
command not only restarts the server but also reloads the Wheels application, ensuring your changes are picked up.
The open
command can open specific paths in your application and works with your preferred browser.
# Display available server commands
wheels server
# Start server on port 8080
wheels server start port=8080
# Check if server is running
wheels server status
# Tail the server logs
wheels server log
# Open admin panel in browser
wheels server open /admin
Server settings can be configured through:
Command line options - Pass options directly to commands
server.json - Create a server.json
file in your project root
box.json - Configure server settings in your box.json
Example server.json
:
{
"web": {
"port": 8080,
"host": "127.0.0.1",
"rewrites": {
"enable": true
}
}
}
These commands are thin wrappers around CommandBox's native server commands, providing:
Validation specific to Wheels applications
Better error messages and guidance
Integration with Wheels-specific features
Consistent command structure
You can always use the native CommandBox commands directly if needed:
# Native CommandBox commands still work
server start
server stop
server status
Check if a server is already running: wheels server status
Try forcing a start: wheels server start --force
Check for port conflicts: wheels server start port=8081
Ensure you're in a directory containing:
/vendor/wheels
- The Wheels framework
/config
- Configuration files
/app
- Application code
Check logs: wheels server log
Verify database connection in datasource settings
Try reloading: wheels reload
Lists installed Wheels CLI plugins or shows available plugins from ForgeBox.
wheels plugins list [--global] [--format=<format>] [--available]
--global
- (Optional) Show globally installed plugins
--format
- (Optional) Output format: table
, json
. Default: table
--available
- (Optional) Show available plugins from ForgeBox
The plugins list
command displays information about all plugins installed in your Wheels application, including:
Plugin name and version
Installation status (active/inactive)
Compatibility with current Wheels version
Description and author information
Dependencies on other plugins
wheels plugins list
wheels plugins list --global
wheels plugins list --format=json
wheels plugins list --available
🧩 Installed Wheels CLI Plugins
Name Version Description
---------------------------------------------
wheels-vue-cli 1.2.0 Vue.js integration for Wheels
wheels-docker 2.0.1 Docker deployment tools
wheels-testing 1.5.0 Advanced testing utilities
Total: 3 plugins
================ Available Wheels Plugins From ForgeBox ======================
[Lists all available cfwheels-plugins from ForgeBox]
=============================================================================
{
"plugins": [
{
"name": "wheels-vue-cli",
"version": "1.2.0",
"description": "Vue.js integration for Wheels"
}
]
}
Local plugins are stored in your project
Global plugins are available to all projects
Use wheels plugins install
to add new plugins
Use wheels plugins remove
to uninstall plugins
The --available
flag queries the ForgeBox registry
Removes an installed Wheels CLI plugin.
wheels plugins remove <name> [--global] [--force]
name
- (Required) Plugin name to remove
--global
- (Optional) Remove globally installed plugin
--force
- (Optional) Force removal without confirmation
The plugins remove
command safely uninstalls a plugin from your Wheels application. It:
Checks for dependent plugins
Creates a backup (by default)
Removes plugin files
Cleans up configuration
Updates plugin registry
wheels plugins remove wheels-vue-cli
wheels plugins remove wheels-docker --global
wheels plugins remove wheels-testing --force
wheels plugins remove wheels-cli-tools --global --force
Dependency Check: Ensures no other plugins depend on this one
Backup Creation: Saves plugin files to backup directory
Deactivation: Disables plugin in application
File Removal: Deletes plugin files and directories
Cleanup: Removes configuration entries
Verification: Confirms successful removal
Are you sure you want to remove the plugin 'wheels-vue-cli'? (y/n): y
🗑️ Removing plugin: wheels-vue-cli...
✅ Plugin removed successfully
🗑️ Removing plugin: wheels-vue-cli...
✅ Plugin removed successfully
Are 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 --global
to remove plugins installed globally
Use wheels plugins list
to verify removal
Some plugins may require manual cleanup of configuration files
Restart your application after removing plugins that affect core functionality
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 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:
The shorthand method would look like:
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:
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 ):
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 and functions:
In this controller's config()
method, we use the 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 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 and , reference the chapter on .
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 p
with the appropriate data.
That is it! Hopefully now you have a clearer picture on how to create AJAX-based features for your web applications.
Reload the Wheels application in different modes.
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.
Enables debugging
Shows detailed error messages
Disables caching
Ideal for active development
Optimized for running tests
Consistent environment
Predictable caching
Shows maintenance page to users
Allows admin access
Useful for deployments
Full caching enabled
Minimal error information
Optimized performance
The 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
:
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
- Initialize application configuration
- Auto-reload on file changes
- Display application information
Setup a complete database by creating it, running migrations, and seeding data.
The wheels db setup
command performs a complete database initialization in one command. It executes three operations in sequence:
Creates the database (wheels db create
)
Runs all migrations (wheels dbmigrate latest
)
Seeds the database with sample data (wheels db seed
)
This is ideal for setting up a new development environment or initializing a test database.
Specify which datasource to use. If not provided, uses the default datasource from your Wheels configuration.
Specify the environment to use. Defaults to the current environment.
Skip the database seeding step.
Number of records to generate per model when seeding. Defaults to 5.
Full setup with default options:
Create and migrate only:
The command executes these steps in order:
Create Database
Creates new database if it doesn't exist
Uses datasource configuration for connection details
Run Migrations
Executes all pending migrations
Creates schema from migration files
Seed Database (unless --skip-seed)
Generates sample data for testing
Creates specified number of records per model
If any step fails:
The command stops execution
Shows which step failed
Provides instructions for manual recovery
Use for development: Perfect for getting started quickly
Skip seeding in production: Use --skip-seed
for production
Customize seed count: More data for performance testing
Check migrations first: Ensure migrations are up to date
- Just create database
- Drop and recreate everything
- Just run migrations
- Just seed data
Show the current database schema version.
The wheels db version
command displays the current version of your database schema based on the last applied migration. This is useful for quickly checking which migration version your database is at.
Show additional information about the database state.
Show current version:
Output:
Output:
Migrations use timestamp-based versions:
Format: YYYYMMDDHHMMSS
Example: 20231203160000
= December 3, 2023 at 4:00:00 PM
If you see "No migrations have been applied yet":
Database is fresh with no migrations run
Run wheels dbmigrate latest
to apply migrations
Before running migrations:
Compare versions across environments:
The version corresponds to:
The latest migration file that has been applied
An entry in the migration tracking table
The current schema state of your database
- Show all migrations and their status
- Update to latest version
- Rollback to previous version
- Detailed migration information
Show the status of database migrations.
The wheels db status
command displays information about the current state of database migrations, showing which migrations have been applied and which are pending.
Output format. Options are table
(default) or json
.
Show only pending migrations.
Show all migrations in table format:
Output:
Output:
Version: Migration timestamp/version number
Description: Human-readable migration name
Status: Either "applied" or "pending"
Applied At: When the migration was run
Green: Applied migrations
Yellow: Pending migrations
Shows counts of:
Total migrations in the migrations folder
Applied migrations in the database
Pending migrations to be run
Check that migration files exist in /db/migrate/
directory
Ensure file naming follows pattern: YYYYMMDDHHMMSS_Description.cfc
If the database version doesn't match expected:
Check migration history in database
Verify no migrations were manually deleted
Consider running wheels dbmigrate latest
- Show just the current version
- Apply pending migrations
- Rollback migrations
- Similar migration 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
Execute a specific database migration by version number.
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.
Move to any point in migration history:
Move to an earlier migration state:
Clear all migrations:
Executing 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
- Run the next migration
- Rollback last migration
- Run all pending migrations
- View migration status
- Create a new migration
List all configuration settings for your Wheels application.
--environment
- (Optional) Environment to display settings for: development
, testing
, production
--filter
- (Optional) Filter results by this string
--show-sensitive
- (Optional) Show sensitive information (passwords, keys, etc.)
The wheels config list
command displays all configuration settings for your Wheels application. It shows current values and helps you understand your application's configuration state.
When using --show-sensitive
, password values are displayed:
The command displays all Wheels configuration settings including:
Database: dataSourceName
, dataSourceUserName
, dataSourcePassword
Cache: cacheQueries
, cacheActions
, cachePages
, cachePartials
Security: reloadPassword
, showDebugInformation
, showErrorInformation
URLs/Routing: urlRewriting
, assetQueryString
, assetPaths
Environment: environment
, hostName
Use the --filter
parameter to search for specific settings:
By default, sensitive values like passwords are masked with asterisks (********
). Use --show-sensitive
to display actual values:
View settings for different environments:
Some settings require restart to take effect
Sensitive values are automatically hidden
Custom settings from plugins included
Performance impact minimal
- Set configuration values
- Environment configuration
- Environment management
Start the Wheels development server with enhanced checks and features.
The wheels server start
command starts a CommandBox server with Wheels-specific enhancements. It checks that you're in a valid Wheels application directory before starting and provides helpful error messages if not.
This command wraps CommandBox's native server start
functionality while adding:
Validation that the current directory is a Wheels application
Automatic detection of existing running servers
Wheels-specific configuration suggestions
Integration with Wheels application context
port
Type: Numeric
Description: Port number to start server on
Example: wheels server start port=8080
host
Type: String
Default: 127.0.0.1
Description: Host/IP address to bind server to
Example: wheels server start host=0.0.0.0
--rewritesEnable
Type: Boolean flag
Description: Enable URL rewriting for clean URLs
Example: wheels server start --rewritesEnable
openbrowser
Type: Boolean
Default: true
Description: Open browser after starting server
Example: wheels server start openbrowser=false
directory
Type: String
Default: Current working directory
Description: Directory to serve
Example: wheels server start directory=/path/to/app
name
Type: String
Description: Name for the server instance
Example: wheels server start name=myapp
--force
Type: Boolean flag
Description: Force start even if server is already running
Example: wheels server start --force
The command validates that the current directory contains a Wheels application by checking for /vendor/wheels
, /config
, and /app
directories
If a server is already running, use --force
to start anyway or wheels server restart
to restart
After starting, the command displays helpful information about other server commands
The server configuration can also be managed through server.json
file
- Stop the server
- Restart the server
- Check server status
- View server logs
- Open in browser
Tail the Wheels development server logs.
The wheels server log
command displays and follows the server log output, making it easy to monitor your application's behavior and debug issues.
name
Type: String
Description: Name of the server whose logs to display
Example: wheels server log name=myapp
--follow
Type: Boolean flag
Default: true
Description: Follow log output (like tail -f)
Example: wheels server log --follow
lines
Type: Numeric
Default: 50
Description: Number of lines to show initially
Example: wheels server log lines=100
--debug
Type: Boolean flag
Description: Show debug-level logging
Example: wheels server log --debug
The logs typically include:
HTTP request/response information
Application errors and stack traces
Database queries (if enabled)
Custom application logging
Server startup/shutdown messages
Ctrl+C - Stop following logs and return to command prompt
Ctrl+L - Clear the screen (while following)
By default, the command follows log output (similar to tail -f
)
Use --debug
to see more detailed logging information
The number of initial lines shown can be customized with the lines
parameter
Logs are stored in the CommandBox server's log directory
- Start the server
- Check server status
- Debug mode for tests
Installs a Wheels CLI plugin from various sources including ForgeBox, GitHub, or local files.
name
- (Required) Plugin name or repository URL
--dev
- (Optional) Install as development dependency
--global
- (Optional) Install globally
--version
- (Optional) Specific version to install
The plugins install
command downloads and installs Wheels plugins into your application. It supports multiple installation sources:
ForgeBox Registry: Official and community plugins
GitHub Repositories: Direct installation from GitHub
Local Files: ZIP files or directories
URL Downloads: Direct ZIP file URLs
The command automatically:
Checks plugin compatibility
Resolves dependencies
Backs up existing plugins
Runs installation scripts
Download: Fetches plugin from specified source
Validation: Checks compatibility and requirements
Backup: Creates backup of existing plugin (if any)
Installation: Extracts files to plugins directory
Dependencies: Installs required dependencies
Initialization: Runs plugin setup scripts
Verification: Confirms successful installation
If installation fails:
Plugins must be compatible with your Wheels version
Always backup your application before installing plugins
Some plugins require manual configuration after installation
Use wheels plugins list
to verify installation
Restart your application to activate new plugins
Analyzes code quality in your Wheels application, checking for best practices, potential issues, and code standards compliance.
path
- (Optional) Path to analyze. Default: current directory (.
)
--fix
- (Optional) Attempt to fix issues automatically
--format
- (Optional) Output format: console
, json
, junit
. Default: console
--severity
- (Optional) Minimum severity level: info
, warning
, error
. Default: warning
--report
- (Optional) Generate HTML report
The analyze code
command performs comprehensive code quality analysis on your Wheels application. It checks for:
Code complexity and maintainability
Adherence to Wheels coding standards
Potential bugs and code smells
Duplicate code detection
Function length and complexity metrics
Variable naming conventions
Deprecated function usage
The command provides detailed feedback including:
Complexity Score: Cyclomatic complexity for functions
Code Standards: Violations of Wheels conventions
Duplicate Code: Similar code blocks that could be refactored
Suggestions: Recommendations for improvement
Metrics Summary: Overall code health indicators
Large codebases may take several minutes to analyze
The --fix
flag will automatically fix issues where possible
HTML reports are saved to the reports/
directory with timestamps
Integration with CI/CD pipelines is supported via JSON and JUnit output formats
Use .wheelscheck
config file for custom rules and configurations
Analyzes application performance, identifying bottlenecks and optimization opportunities in your Wheels application.
--target
- (Optional) Analysis target: all
, controller
, view
, query
, memory
. Default: all
--duration
- (Optional) Duration to run analysis in seconds. Default: 30
--report
- (Optional) Generate HTML performance report
--threshold
- (Optional) Performance threshold in milliseconds. Default: 100
--profile
- (Optional) Enable profiling mode
The analyze performance
command profiles your Wheels application to identify performance bottlenecks and provide optimization recommendations. It monitors:
Request execution times
Database query performance
Memory usage patterns
Cache effectiveness
View rendering times
Component instantiation overhead
The analysis provides:
Slowest Requests: Top 10 slowest request paths
Query Analysis: Slow queries and N+1 query detection
Memory Hotspots: Areas of high memory allocation
Cache Statistics: Hit/miss ratios for various caches
Recommendations: Specific optimization suggestions
Profiling adds minimal overhead to your application
Best run in a staging environment with production-like data
Can be integrated with APM tools for continuous monitoring
Results are aggregated across all application instances
Tutorials, demonstrations, and presentations about the ColdFusion on Wheels framework.
Create a basic CRUD interface in Wheels 2.x
Create a basic JSON API in Wheels 2.x
Routing in Wheels 2.x - Part 1
Routing in Wheels 2.x - Part 2
Introduction to Unit Testing in Wheels 2.x
Unit Testing Controllers in Wheels 2.x
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.
Learn about basic create operations when building standard CRUD functionality in Wheels
Learn about basic read operations when building standard CRUD functionality in Wheels
Chris Peters demonstrates updating data in a simple CRUD Wheels application
Learn how simple it is to delete records in a basic CRUD application using Wheels
Chris Peters starts the webcast series by demonstrating how to set up ColdFusion on Wheels
Chris Peters demonstrates how to bind a Wheels model object to a form through the use of form helpers
Chris Peters adds data validation to the user registration form
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
Chris Peters teaches you about more validation options and how you can add them to the registration form quickly and easily
Chris Peters stylizes form markup globally using a Wheels feature called global helpers
Learn how to set up simple user authentication on a website by using a Wheels feature called filters
Learn the mechanics of reading a single record from the database and displaying its data in the view
Creating custom URL patterns is a breeze in ColdFusion on Wheels
Learn how to fetch multiple records from your model with findAll() and then display them to the user using ColdFusion on Wheels
Learn how to factor out logic in your view templates into custom helper functions in ColdFusion on Wheels
Chris Peters demonstrates joining data together with model associations using ColdFusion on Wheels
All it takes to offer pagination is two extra arguments to findAll() and a call to a view helper called paginationLinks()
Learn how to use the provides() and renderWith() functions to automatically serialize data into XML, JSON, and more
Peter Amiri walks you through setting up a "Hello World" application using the ColdFusion on Wheels framework
Chris Peters gives a high level overview of the ORM included with ColdFusion on Wheels
Chris Peters from Liquifusion demonstrates the ColdRoute plugin for Wheels
Doug Boude demonstrates using his new Wirebox plugin for Wheels
Chris Peters from Liquifusion demonstrates creating tables and records using database migrations in ColdFusion on Wheels
Online ColdFusion Meetup (coldfusionmeetup.com) session for March 10 2011, "What's New in Wheels 1.1", with Chris Peters:
A quick demo of the Wheels Textmate bundle by Russ Johnson
wheels plugins install <name> [--dev] [--global] [--version=<version>]
wheels plugins install wheels-vue-cli
wheels plugins install wheels-docker --version=2.0.0
wheels plugins install https://github.com/user/wheels-plugin
wheels plugins install wheels-docker --dev
wheels plugins install wheels-cli-tools --global
wheels plugins install wheels-testing --dev --version=1.5.0
📦 Installing plugin: wheels-vue-cli...
✅ Plugin installed successfully
📦 Installing plugin: invalid-plugin...
❌ Plugin installation failed
Error: Plugin not found in repository
# Install by name (searches ForgeBox)
wheels plugins install plugin-name
# Install specific ForgeBox ID
wheels plugins install forgebox:plugin-slug
# HTTPS URL
wheels plugins install https://github.com/user/repo
# GitHub shorthand
wheels plugins install github:user/repo
# Specific branch/tag
wheels plugins install github:user/repo#v2.0.0
wheels plugins install https://example.com/plugin.zip
wheels analyze code [path] [--fix] [--format=<format>] [--severity=<severity>] [--report]
wheels analyze code
wheels analyze code app/controllers
wheels analyze code --fix
wheels analyze code --report
wheels analyze code --format=json
wheels analyze code --severity=error
wheels analyze code app/models --fix --report
wheels analyze performance [--target=<target>] [--duration=<seconds>] [--report] [--threshold=<ms>] [--profile]
wheels analyze performance
wheels analyze performance --duration=60 --profile
wheels analyze performance --target=query
wheels analyze performance --threshold=500
wheels analyze performance --report
wheels analyze performance --target=all --duration=60 --threshold=200 --profile --report
Performance Analysis Results
===========================
Slowest Requests:
1. GET /users/search (avg: 850ms, calls: 45)
2. POST /orders/create (avg: 650ms, calls: 12)
3. GET /reports/generate (avg: 1200ms, calls: 8)
Database Issues:
- N+1 queries detected in UsersController.index
- Slow query in Order.findRecent() - 450ms avg
- Missing index suggested for users.created_at
Memory Usage:
- High allocation in ReportService.generate()
- Potential memory leak in SessionManager
Recommendations:
1. Add eager loading to UsersController.index
2. Create index on users.created_at
3. Implement query result caching for Order.findRecent()
mapper()
.get(name="sayHello", controller="say", action="hello")
.end()
mapper()
.get(name="sayHello", to="say##hello")
.end()
<cfoutput>
<!--- View code --->
<h1></h1>
<p></p>
#linkTo(text="Alert me!", route="sayHello", id="alert-button")#
</cfoutput>
(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);
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);
}
}
wheels reload [options]
wheels r [options]
mode
Reload mode: development
, testing
, maintenance
, production
development
password
Required - The reload password configured in your application
None
--help
Show help information
wheels reload password=mypassword
wheels reload mode=testing password=mypassword
wheels reload mode=maintenance password=mypassword
wheels reload mode=production password=mypassword
wheels reload password=wheels
wheels reload mode=production password=mySecretPassword
wheels r password=wheels
wheels reload mode=testing password=wheels
set(reloadPassword="mySecretPassword");
wheels db setup [--datasource=<name>] [--environment=<env>] [--skip-seed] [--seed-count=<n>]
wheels db setup --datasource=myapp_dev
wheels db setup --environment=testing
wheels db setup --skip-seed
wheels db setup --seed-count=20
wheels db setup
wheels db setup --skip-seed
wheels db setup --datasource=myapp_test --environment=testing --seed-count=10
wheels db setup --environment=production --skip-seed
git clone https://github.com/myproject/repo.git
cd repo
box install
wheels db setup
server start
wheels db drop --force
wheels db setup --seed-count=50
# In CI script
wheels db setup --environment=testing --skip-seed
wheels test run
wheels db version [--detailed]
wheels db version --detailed
wheels db version
Current database version: 20231203160000
wheels db version --detailed
Current database version: 20231203160000
Last migration:
Version: 20231203160000
Description: CreatePostsTable
Applied at: 2023-12-03 16:45:32
Total migrations: 15
Pending migrations: 2
Next migration to apply:
Version: 20231204180000
Description: AddIndexToPostsUserId
Environment: development
Datasource: myapp_dev
wheels db version
wheels dbmigrate latest
# Check production is up to date
wheels db version --environment=production --detailed
# Development
wheels db version --environment=development
# Staging
wheels db version --environment=staging
# Production
wheels db version --environment=production
wheels db status [--format=<format>] [--pending]
wheels db status --format=json
wheels db status --pending
wheels db status
Current database version: 20231203160000
| Version | Description | Status | Applied At |
|--------------------|----------------------------------|----------|-------------------|
| 20231201120000 | CreateUsersTable | applied | 2023-12-01 12:30 |
| 20231202140000 | AddEmailToUsers | applied | 2023-12-02 14:15 |
| 20231203160000 | CreatePostsTable | applied | 2023-12-03 16:45 |
| 20231204180000 | AddIndexToPostsUserId | pending | Not applied |
Total migrations: 4
Applied: 3
Pending: 1
wheels db status --pending
wheels db status --format=json
{
"success": true,
"currentVersion": "20231203160000",
"migrations": [
{
"version": "20231201120000",
"description": "CreateUsersTable",
"status": "applied",
"appliedAt": "2023-12-01 12:30:00"
},
{
"version": "20231204180000",
"description": "AddIndexToPostsUserId",
"status": "pending",
"appliedAt": null
}
],
"summary": {
"total": 4,
"applied": 3,
"pending": 1
}
}
# See what migrations will run in production
wheels db status --environment=production --pending
# Check if specific migration was applied
wheels db status | grep "AddEmailToUsers"
# Get pending count for automation
wheels db status --format=json | jq '.summary.pending'
wheels dbmigrate down
wheels 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 info
wheels dbmigrate exec version=<version>
version
string
Yes
Version to migrate to
wheels dbmigrate exec version=20240115123456
wheels dbmigrate exec version=0
# Check current status
wheels dbmigrate info
# Migrate to specific version
wheels dbmigrate exec version=20240115123456
# Check migration history
wheels dbmigrate info
# Go back to specific version
wheels dbmigrate exec version=20240101000000
# Migrate to version 0
wheels dbmigrate exec version=0
# Verify empty state
wheels dbmigrate info
wheels config list [--environment=<env>] [--filter=<pattern>] [--show-sensitive]
wheels config list
wheels config list --filter=cache
wheels config list --filter=database
wheels config list --show-sensitive
wheels config list --environment=production
wheels config list --environment=testing
wheels config list --environment=production --filter=cache --show-sensitive
Wheels Configuration Settings
============================
Setting Value
-------------------------------- --------------------------------
dataSourceName wheels_dev
environment development
reloadPassword ********
showDebugInformation true
showErrorInformation true
cacheFileChecking false
cacheQueries false
cacheActions false
urlRewriting partial
assetQueryString true
assetPaths true
reloadPassword mySecretPassword123
# Find all cache-related settings
wheels config list --filter=cache
# Find datasource settings
wheels config list --filter=datasource
# Show all settings including passwords
wheels config list --show-sensitive
# Production settings
wheels config list --environment=production
# Testing environment
wheels config list --environment=testing
wheels server start [options]
# Start server with defaults
wheels server start
# Start on specific port
wheels server start port=3000
# Start without opening browser
wheels server start openbrowser=false
# Start with multiple options
wheels server start port=8080 host=0.0.0.0 --rewritesEnable
# Start with custom name
wheels server start name=myapp port=8080
# Force restart if already running
wheels server start --force
wheels server log [options]
# Follow logs (default behavior)
wheels server log
# Show last 100 lines
wheels server log lines=100
# Show logs without following
wheels server log --follow=false
# Enable debug logging
wheels server log --debug
Open the Wheels application in a web browser.
wheels server open [path] [options]
The wheels server open
command opens your Wheels application in a web browser. It automatically detects the server URL and can open specific paths within your application.
path
Type: String
Default: /
Description: URL path to open (e.g., /admin
, /users
)
Example: wheels server open /admin
--browser
Type: String
Description: Specific browser to use (chrome, firefox, safari, etc.)
Example: wheels server open --browser=firefox
name
Type: String
Description: Name of the server to open
Example: wheels server open name=myapp
# Open application homepage
wheels server open
# Open admin panel
wheels server open /admin
# Open users listing
wheels server open /users
# Open in specific browser
wheels server open --browser=chrome
# Open path in Firefox
wheels server open /dashboard --browser=firefox
The --browser
option supports:
chrome
- Google Chrome
firefox
- Mozilla Firefox
safari
- Safari (macOS)
edge
- Microsoft Edge
opera
- Opera
If no browser is specified, your system's default browser is used.
The command first checks if the server is running before attempting to open
If the server isn't running, it will suggest starting it first
The path argument should start with /
for proper URL construction
The browser must be installed on your system to use the --browser
option
The server must be started before you can open it in a browser:
wheels server start
wheels server open
wheels server start
- Start the server
wheels server status
- Check if server is running
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.
Remove generated code and files associated with a model, controller, views, and tests.
wheels destroy <name>
wheels d <name>
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.
name
Name of the resource to destroy
Yes
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/Testbox/specs/models/[Name].cfc
)
Controller test file (/tests/Testbox/specs/controllers/[Names].cfc
)
View test directory (/tests/Testbox/specs/views/[names]/
)
Resource route entry in /config/routes.cfm
Database table (if confirmed)
wheels destroy user
This will prompt:
================================================
= 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/Testbox/specs/models/User.cfc
/tests/Testbox/specs/controllers/Users.cfc
/tests/Testbox/specs/views/users/
/config/routes.cfm
.resources("users")
Are you sure? [y/n]
wheels d product
The command always asks for confirmation and shows exactly what will be deleted:
================================================
= 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/Testbox/specs/models/User.cfc
/tests/Testbox/specs/controllers/Users.cfc
/tests/Testbox/specs/views/users/
/config/routes.cfm
.resources("users")
Are you sure? [y/n]
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
# 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_post
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
wheels generate resource - Generate resources
wheels generate scaffold - Generate scaffolding
wheels dbmigrate remove table - Remove database tables
Rollback database migrations to a previous state.
wheels db rollback [--steps=<n>] [--target=<version>] [--force]
The wheels db rollback
command reverses previously applied migrations by running their down
methods. You can rollback a specific number of migrations or to a specific version.
Number of migrations to rollback. Defaults to 1.
wheels db rollback --steps=3
Rollback to a specific migration version.
wheels db rollback --target=20231201120000
Skip the confirmation prompt.
wheels db rollback --force
Rollback the last migration:
wheels db rollback
Rollback the last 3 migrations:
wheels db rollback --steps=3
Rollback to a specific point in time:
wheels db rollback --target=20231201120000
Skip confirmation:
wheels db rollback --steps=5 --force
Identifies Migrations: Determines which migrations to rollback
Confirmation: Asks for confirmation (unless --force)
Executes Down Methods: Runs the down()
method of each migration in reverse order
Updates Version: Updates the database version tracking
Rollbacks can result in data loss if migrations:
Drop tables
Remove columns
Delete records
Always backup before rolling back:
wheels db dump --output=backup-before-rollback.sql
wheels db rollback --steps=3
For rollback to work, migrations must:
Have a properly implemented down()
method
Be reversible (some operations can't be undone)
Example migration with down method:
component {
function up() {
addColumn(table="users", columnName="age", columnType="integer");
}
function down() {
removeColumn(table="users", columnName="age");
}
}
Made a mistake in the last migration:
# Rollback
wheels db rollback
# Fix the migration file
# Edit: db/migrate/20231204180000_AddAgeToUsers.cfc
# Run it again
wheels dbmigrate latest
Remove a feature and its database changes:
# Rollback feature migrations
wheels db rollback --target=20231201120000
# Remove feature code
# Deploy
Test that migrations are reversible:
# Apply migration
wheels dbmigrate latest
# Test rollback
wheels db rollback
# Reapply
wheels dbmigrate latest
If you see this error:
The migration doesn't have a down()
method
Add the method to make it reversible
Or use wheels db reset
if in development
Some operations can't be reversed:
Data deletions (unless backed up in migration)
Complex transformations
External system changes
If rollback fails partway:
Check error message for specific issue
Fix the migration's down method
Manually correct database if needed
Update migration tracking table
Always implement down methods in migrations
Test rollbacks in development before production
Backup before rollback in production
Document irreversible changes in migrations
Use transactions in complex rollbacks
wheels db status
- Check current migration state
wheels dbmigrate down
- Similar single rollback
wheels db reset
- Full database reset
wheels db dump
- Backup before rollback
Reset all database migrations by migrating to version 0.
wheels dbmigrate reset
Alias: 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 reset
This 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 seed
Verify 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 needed
When migrations have dependency problems:
# Reset all migrations
wheels dbmigrate reset
# Manually fix migration files
# Re-run all migrations
wheels dbmigrate latest
Reset database for each test run:
# CI script
wheels dbmigrate reset
wheels dbmigrate latest
wheels test run
WARNING: 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
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, etc.)
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 schema_migrations
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 schema_migrations
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
wheels dbmigrate info
+-----------------------------------------+-----------------------------------------+
| Datasource: myapp_development | Total Migrations: 6 |
| Database Type: MySQL | Available Migrations: 2 |
| | Current Version: 20240115120000 |
| | Latest Version: 20240125160000 |
+-----------------------------------------+-----------------------------------------+
+----------+------------------------------------------------------------------------+
| migrated | 20240101100000_create_users_table.cfc |
| migrated | 20240105150000_create_products_table.cfc |
| migrated | 20240110090000_add_email_to_users.cfc |
| migrated | 20240115120000_create_orders_table.cfc |
| | 20240120140000_add_status_to_orders.cfc |
| | 20240125160000_create_categories_table.cfc |
+----------+------------------------------------------------------------------------+
[timestamp]_[description].cfc
20240125160000_create_users_table.cfc
SELECT * FROM schema_migrations;
+----------------+
| version |
+----------------+
| 20240101100000 |
| 20240105150000 |
| 20240110090000 |
| 20240115120000 |
+----------------+
wheels dbmigrate info
wheels dbmigrate latest
wheels dbmigrate info
Current Version: 20240125160000
Status: 6 completed, 0 pending
✓ Database is up to date
Current Version: 0
Status: 0 completed, 6 pending
⚠ No migrations have been run
Current Version: 20240110090000
Status: 3 completed, 3 pending
⚠ Database needs migration
#!/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
fi
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 list
Output shows:
Package name
Version specification
Type (Production/Development)
Installation status
Example output:
Dependencies:
wheels-core @ ^3.0.0 (Production) - Installed
wirebox @ ^7 (Production) - Installed
testbox @ ^5 (Production) - Installed
Dev Dependencies:
commandbox-dotenv @ * (Development) - Not Installed
commandbox-cfformat @ * (Development) - Installed
Install a new dependency and add it to box.json.
wheels deps install <name>
wheels deps install <name> <version>
wheels deps install <name> --dev
Examples:
# Install latest version as production dependency
wheels deps install cbvalidation
# Install specific version
wheels deps install cbvalidation 3.0.0
# Install as development dependency
wheels deps install testbox --dev
Update an existing dependency to the latest version allowed by its version specification.
wheels deps update <name>
Example:
wheels deps update wirebox
The 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 oldpackage
Note: Remove action will ask for confirmation before proceeding.
Generate a comprehensive dependency report with outdated package check.
wheels deps report
The 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-06-04 10:05:35
Project: my-wheels-app
Project Version: 1.0.0
Wheels Version: 3.0.0
CFML Engine: Lucee 5.4.6.9
Dependencies:
wheels-core @ ^3.0.0 - Installed: Yes
wirebox @ ^7 - Installed: Yes
Dev Dependencies:
testbox @ ^5 - Installed: Yes
Checking for outdated packages...
All packages are up to date!
Full report exported to: dependency-report-20250604-100535.json
The wheels deps
commands delegate to CommandBox's package management system:
install
uses box install
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": "^5",
"commandbox-cfformat": "*"
}
}
The command checks for installed packages in the /modules
directory. It handles various package naming conventions:
Simple names: wirebox
Namespaced: forgebox:wirebox
Versioned: [email protected]
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
Export database schema and data to a file.
wheels db dump [--output=<file>] [--datasource=<name>] [--environment=<env>]
[--schema-only] [--data-only] [--tables=<list>] [--compress]
The wheels db dump
command exports your database to a SQL file that can be used for backups, migrations, or setting up new environments. It supports various options for customizing what gets exported.
Output file path. Defaults to dump_[datasource]_[timestamp].sql
.
wheels db dump --output=backup.sql
Specify which datasource to dump. If not provided, uses the default datasource.
wheels db dump --datasource=myapp_prod
Specify the environment to use. Defaults to the current environment.
wheels db dump --environment=production
Export only the database structure (no data).
wheels db dump --schema-only
Export only the data (no structure).
wheels db dump --data-only
Comma-separated list of specific tables to dump.
wheels db dump --tables=users,posts,comments
Compress the output file using gzip.
wheels db dump --compress
Create a timestamped backup:
wheels db dump
# Creates: dump_myapp_dev_20231204153045.sql
wheels db dump --environment=production --output=prod-backup.sql --compress
For version control:
wheels db dump --schema-only --output=schema.sql
Export user-related tables:
wheels db dump --tables=users,user_roles,user_sessions --output=user-data.sql
Save space with compression:
wheels db dump --output=backup.sql.gz --compress
Uses mysqldump
utility
Includes stored procedures and triggers
Preserves character sets and collations
Uses pg_dump
utility
Includes schemas, functions, and extensions
Handles permissions and ownership
Basic export functionality
For full backups, use SQL Server Management Studio
Uses built-in SCRIPT command
Exports to SQL script format
Supports compression natively
The dump file contains:
Database structure (CREATE TABLE statements)
Indexes and constraints
Data (INSERT statements)
Views, procedures (if supported)
Example output structure:
-- Database dump generated by Wheels
-- Date: 2023-12-04 15:30:45
-- Table structure for users
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
email VARCHAR(255) UNIQUE
);
-- Data for users
INSERT INTO users (id, name, email) VALUES
(1, 'John Doe', '[email protected]'),
(2, 'Jane Smith', '[email protected]');
Automated backup script:
#!/bin/bash
DATE=$(date +%Y%m%d)
wheels db dump --output=backups/daily-$DATE.sql.gz --compress
# Before deployment
wheels db dump --output=pre-deployment-backup.sql
# Deploy changes
# ...
# If something goes wrong
wheels db restore pre-deployment-backup.sql
# Export from production
wheels db dump --environment=production --output=prod-data.sql
# Import to staging
wheels db restore prod-data.sql --environment=staging
# Export specific tables
wheels db dump --tables=legacy_users,legacy_orders --output=migration-data.sql
# Process with migration scripts
# Import to new structure
Large databases may take time to dump
Use --compress
to reduce file size
Consider --tables
for partial backups
Off-peak hours for production dumps
Protect dump files - They contain sensitive data
Encrypt backups - Use additional encryption for sensitive data
Secure transfer - Use secure methods to transfer dumps
Clean up - Don't leave dump files in public directories
Install MySQL client tools:
# macOS
brew install mysql-client
# Linux
sudo apt-get install mysql-client
Check database user has SELECT permissions
Ensure write permissions for output directory
Use --compress
to reduce size
Consider table-by-table exports
Increase timeout settings if needed
wheels db restore
- Restore from dump
wheels db status
- Check before dump
wheels db shell
- Manual export options
Restore a database from a dump file.
wheels db restore <file> [--datasource=<name>] [--environment=<env>]
[--clean] [--force] [--compressed]
The wheels db restore
command imports a database dump file created by wheels db dump
or other database export tools. It can handle both plain SQL files and compressed dumps.
Required. Path to the dump file to restore.
wheels db restore backup.sql
Specify which datasource to restore to. If not provided, uses the default datasource.
wheels db restore backup.sql --datasource=myapp_dev
Specify the environment to use. Defaults to the current environment.
wheels db restore backup.sql --environment=staging
Drop existing database objects before restore.
wheels db restore backup.sql --clean
Skip confirmation prompts.
wheels db restore backup.sql --force
Indicate the file is compressed. Auto-detected for .gz files.
wheels db restore backup.sql.gz --compressed
wheels db restore backup.sql
# Auto-detects compression from .gz extension
wheels db restore backup.sql.gz
# Or explicitly specify
wheels db restore backup.sql --compressed
Drop existing objects first:
wheels db restore backup.sql --clean
Skip confirmation in scripts:
wheels db restore backup.sql --force
wheels db restore prod-backup.sql --environment=staging --force
Confirmation Required: Prompts before overwriting data
Production Warning: Extra warning for production environments
File Validation: Checks file exists before starting
Uses mysql
client
Handles large files efficiently
Preserves character sets
Uses psql
client
Supports custom formats from pg_dump
Handles permissions and ownership
Uses sqlcmd
client
Limited support for complex backups
Best with SSMS for full restores
Uses RUNSCRIPT command
Native support for compressed files
Fast for embedded databases
# Get fresh production data
wheels db dump --environment=production --output=prod-latest.sql
wheels db restore prod-latest.sql --environment=development --clean
# Restore from latest backup
wheels db restore backups/daily-20231204.sql.gz --force
# Clone staging to test
wheels db dump --environment=staging --output=staging-snapshot.sql
wheels db restore staging-snapshot.sql --environment=test --clean
Restoring overwrites existing data
Always backup current database first
Use --clean
carefully
Ensure dump is from compatible database version
Check character set compatibility
Verify schema matches application version
Monitor disk space during restore
Consider using --compressed
dumps
May need to adjust timeout settings
Backup current database
wheels db dump --output=pre-restore-backup.sql
Verify dump file
ls -lh backup.sql
head -n 20 backup.sql # Check format
Check disk space
df -h # Ensure enough space
Stop application (if needed)
server stop
Check database user has CREATE/DROP permissions
Verify credentials in datasource
Check file path is correct
Use absolute paths for clarity
Verify dump file isn't corrupted
Check database version compatibility
Ensure correct database type
Large databases take time
Check database server resources
Monitor with wheels db shell
in another terminal
Ensure database charset matches dump
Check connection encoding settings
Test restores regularly - Verify backups work
Document source - Note where dumps came from
Version control - Track schema version with dumps
Automate testing - Script restore verification
Secure dumps - Protect sensitive data in dumps
If full restore fails:
# Extract specific tables from dump
grep -E "(CREATE TABLE|INSERT INTO) users" backup.sql > users-only.sql
wheels db restore users-only.sql
Use database shell for control:
wheels db shell
# Then manually run SQL from dump file
wheels db dump
- Create database dumps
wheels db reset
- Alternative fresh start
wheels db shell
- Manual restore control
wheels db status
- Verify after restore
Generate a migration file for dropping a database table.
wheels dbmigrate remove table name=<table_name>
Alias: wheels db remove table
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_data
wheels dbmigrate remove table name=user
wheels dbmigrate remove table name=orders_archive_2023
For the command:
wheels dbmigrate remove table name=product_archive
Generates:
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_20240115
Remove 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_old
Remove tables from cancelled features:
# Remove tables from abandoned feature
wheels dbmigrate remove table name=beta_feature_data
wheels dbmigrate remove table name=beta_feature_settings
Remove old archive tables:
# Remove yearly archive tables
wheels dbmigrate remove table name=orders_archive_2020
wheels dbmigrate remove table name=orders_archive_2021
CRITICAL: 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 removed
Before 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_preferences
For 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_deprecated
Verify 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_preferences
The 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
⚠️ 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.
wheels analyze security [path] [--fix] [--report=<format>] [--severity=<level>] [--deep]
wheels security scan [path] [--fix] [--output=<format>] [--detailed]
When called, the deprecated command forwards parameters to the new command:
path
→ path
--fix
→ --fix
--report
→ --output
--severity
→ (handled internally)
--deep
→ --detailed
Better command organization with dedicated security namespace
Enhanced scanning capabilities
Improved reporting options
Integration with security vulnerability databases
security scan - 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.
Stop the Wheels development server.
wheels server stop [options]
The wheels server stop
command stops a running CommandBox server. It provides a simple way to shut down your development server cleanly.
name
Type: String
Description: Name of the server to stop (if multiple servers are running)
Example: wheels server stop name=myapp
--force
Type: Boolean flag
Description: Force stop all running servers
Example: wheels server stop --force
# Stop the default server
wheels server stop
# Stop a specific named server
wheels server stop name=myapp
# Force stop all servers
wheels server stop --force
If no server name is specified, stops the server in the current directory
Use --force
to stop all running servers at once
The command will confirm when the server has been stopped successfully
wheels server start
- Start the server
wheels server restart
- Restart the server
wheels server status
- Check server status
Watch Wheels application files for changes and automatically reload the application.
The wheels watch
command monitors your application files for changes and automatically triggers actions like reloading the application, running tests, or executing custom commands. This provides a smooth development workflow with instant feedback.
Note: In CommandBox, boolean flags are specified with --flagname
and value parameters with paramname=value
.
Watches default directories for changes and reloads the application
Runs tests automatically when files change
The watch command starts with:
An initial scan of all watched directories to establish baseline
Displays count of files being monitored
Checks for changes at the specified interval
When changes are detected:
With --reload
: Reloads the application
With --tests
: Runs tests (smart filtering based on changed files)
With --migrations
: Runs migrations if schema files changed
With --command
: Executes the specified command
The excludeFiles
parameter supports patterns:
*.txt
- Exclude all .txt files
*.log
- Exclude all .log files
temp.cfc
- Exclude specific file name
Multiple patterns: excludeFiles="*.txt,*.log,temp.*"
When --tests
is enabled, the command intelligently determines which tests to run:
Changes to models run model tests
Changes to controllers run controller tests
Multiple changes batch test execution
With --migrations
enabled, the command detects:
New migration files in /migrator/migrations/
Changes to schema files
Automatically runs wheels dbmigrate up
Initial scan time depends on project size
Use includeDirs
to limit scope
Use excludeFiles
to skip large files
Adjust interval
for less frequent checks
Use debounce
to batch rapid changes
Start Simple: Use wheels watch
with defaults first
Add Features Gradually: Enable tests, migrations as needed
Optimize Scope: Use includeDirs
for faster performance
Exclude Wisely: Skip log files, temp files, etc.
Batch Changes: Increase debounce for multiple file saves
High CPU Usage: Reduce check frequency with interval
Missed Changes: Check excluded patterns
Reload Errors: Ensure reload password is configured
Test Failures: Run tests manually to debug
Changes are tracked by file modification time
New files are automatically detected
Deleted files are removed from tracking
Press Ctrl+C to stop watching
- Manual application reload
- Run tests manually
- Run migrations
Launch an interactive database shell for direct SQL access to your database.
The wheels db shell
command provides direct access to your database through its native command-line interface. It automatically detects your database type and launches the appropriate shell client.
For H2 databases (commonly used with Lucee), it can also launch a web-based console interface.
Specify which datasource to connect to. If not provided, uses the default datasource from your Wheels configuration.
Specify the environment to use. Defaults to the current environment.
For H2 databases only, launches the web-based console interface instead of the CLI shell.
Execute a single SQL command and exit, rather than entering interactive mode.
For H2 databases (default with Lucee), you have two options:
CLI Shell:
Provides a command-line SQL interface
Type SQL commands directly
Use help
for available commands
Exit with exit
or Ctrl+D
Web Console:
Opens H2's web interface in your default browser
Provides a GUI for browsing tables and running queries
More user-friendly for complex operations
Press Ctrl+C in terminal to stop the console server
Launches the mysql
client
Connects using datasource credentials
Full MySQL command-line interface
Exit with exit
, quit
, or Ctrl+D
Launches the psql
client
Connects using datasource credentials
Full PostgreSQL command-line interface
Type \h
for help, \q
to quit
Launches the sqlcmd
client
Connects using datasource credentials
Full SQL Server command-line interface
Type :help
for help, :quit
to exit
Launch shell for default datasource:
Open H2's web interface:
Get row count without entering interactive mode:
Check database version:
Connect to test database:
Connect to production (with caution):
Once in the shell, here are some useful commands:
The shell command requires database-specific client tools:
H2: No additional installation (included with Lucee)
MySQL: Install mysql
client
PostgreSQL: Install psql
client
SQL Server: Install sqlcmd
If you get errors about mysql/psql/sqlcmd not being found:
Install the appropriate client tool (see Requirements above)
Ensure the tool is in your PATH
Restart your terminal/command prompt
If the H2 shell fails to connect:
Check that your datasource is properly configured
Ensure the database file exists (check db/h2/
directory)
Try the web console instead: wheels db shell --web
If you get authentication errors:
Verify datasource credentials in your CFML admin
Check that the database user has appropriate permissions
For PostgreSQL, you might need to set PGPASSWORD environment variable
If you get "H2 JAR not found" error:
Ensure H2 is installed as a Lucee extension
Check for org.lucee.h2-*.jar
in Lucee's lib directory
Try reinstalling the H2 extension through Lucee admin
Be careful in production: The shell provides full database access
Avoid hardcoding credentials: Use datasource configuration
Limit permissions: Database users should have appropriate restrictions
Audit usage: Shell commands may not be logged by your application
- Create a new database
- Check migration status
- Export database
- Run migrations
- CFML interactive console
Visualize the current database schema.
The wheels db schema
command retrieves and displays the current database schema in various formats. This is useful for documentation, debugging, and understanding your database structure.
Displays a human-readable table structure:
Generates CREATE TABLE statements:
Provides structured schema information:
Track schema changes in git:
Generate schema documentation:
Create schema backups before major updates:
Quickly review your database structure:
Currently, the command exports all tables in the database. Future versions may support filtering specific tables.
Export schema after migrations:
Track schema changes over time:
Keep a record of schema state:
Migrations: Track incremental changes over time
Schema: Shows current database state
Create migration: wheels dbmigrate create table name=users
Edit and run migration: wheels dbmigrate up
Export current schema: wheels db schema --save file=db/schema.sql
Commit both: git add app/migrator/migrations/* db/schema.sql
Schema export captures current database state
The command connects to your configured datasource
Output varies based on your database type (MySQL, PostgreSQL, etc.)
Some database-specific features may require manual adjustment
Use migrations for incremental changes, schemas for documentation
- Run all migrations
- View migration status
- Seed database with data
- Generate models from schema
Start an interactive REPL console with Wheels application context loaded.
The wheels console
command starts an interactive Read-Eval-Print Loop (REPL) with your Wheels application context fully loaded. This allows you to interact with your models, run queries, test helper functions, and debug your application in real-time.
The console requires your Wheels server to be running as it connects via HTTP to maintain the proper application context.
environment
Type: String
Default: development
Description: Environment to load (development, testing, production)
Example: wheels console environment=testing
execute
Type: String
Description: Execute a single command and exit
Example: wheels console execute="model('User').count()"
script
Type: Boolean
Default: true
Description: Use CFScript mode (false for tag mode)
Example: wheels console script=false
directory
Type: String
Default: Current working directory
Description: Application directory
Example: wheels console directory=/path/to/app
Once in the console, you can use these special commands:
help
or ?
- Show help information
examples
- Show usage examples
script
- Switch to CFScript mode
tag
- Switch to tag mode
clear
or cls
- Clear screen
history
- Show command history
exit
, quit
, or q
- Exit console
The console connects to your running Wheels server via HTTP
Code is sent to a special console endpoint for execution
The code runs in the full application context with access to all models and helpers
Results are returned and displayed in the console
Variables persist between commands during the session
Wheels server must be running (wheels server start
)
Server must be accessible at the configured URL
Application must be in development, testing, or maintenance mode
Start your server first:
Check that your server is running: wheels server status
Verify the server URL is correct
Ensure your application is not in production mode
Check syntax - console shows line numbers for errors
Remember you're in CFScript mode by default
Use tag
command to switch to tag mode if needed
Use for debugging: Test model methods and queries before implementing
Data exploration: Quickly inspect and modify data during development
Testing helpers: Verify helper function outputs
Learning tool: Explore Wheels functionality interactively
Avoid in production: Console should not be accessible in production mode
Console is only available in development, testing, and maintenance modes
Never expose console access in production environments
Be cautious when manipulating production data
- Execute script files
- Manage environment settings
- Start the server
Scans your Wheels application for security vulnerabilities and provides remediation recommendations.
path
- (Optional) Path to scan. Default: current directory (.
)
--fix
- (Optional) Attempt to fix issues automatically
--report
- (Optional) Report format: console
, json
, html
. Default: console
--severity
- (Optional) Minimum severity to report: low
, medium
, high
, critical
. Default: medium
--output
- (Optional) Output file for report
The security scan
command performs comprehensive security analysis of your Wheels application, checking for:
SQL injection vulnerabilities
Cross-site scripting (XSS) risks
Cross-site request forgery (CSRF) issues
Insecure direct object references
Security misconfigurations
Outdated dependencies with known vulnerabilities
Weak authentication patterns
Information disclosure risks
The --severity
parameter filters which issues are reported:
Code style issues that could lead to vulnerabilities
Missing best practices
Informational findings
Potential security issues requiring review
Missing security headers
Weak configurations
Confirmed vulnerabilities with moderate impact
Authentication/authorization issues
Data validation problems
Severe vulnerabilities requiring immediate attention
SQL injection risks
Remote code execution possibilities
The --fix
flag automatically resolves safe issues:
Generates interactive HTML report with:
Executive summary
Detailed findings with code snippets
Remediation steps
Compliance mapping (OWASP, CWE)
Machine-readable format for CI/CD integration
Scans are performed locally; no code is sent externally
False positives can be suppressed with inline comments
Regular scanning is recommended as part of development workflow
Keep scan rules updated with wheels deps update
Some fixes require manual review to ensure functionality
wheels security scan [path] [--fix] [--report=<format>] [--severity=<level>] [--output=<file>]
wheels security scan
wheels security scan --fix
wheels security scan --report=html --output=security-audit.html
wheels security scan app/models --severity=high
wheels security scan --report=json --output=scan-results.json
Security Scan Results
====================
Scanning application...
✓ Configuration files
✓ Controllers (15 files)
✓ Models (8 files)
✓ Views (23 files)
✗ Dependencies (2 issues)
CRITICAL: 1 issue found
-----------------------
1. SQL Injection Risk
File: /app/models/User.cfc
Line: 45
Code: findOne(where="id = #params.id#")
Fix: Use parameterized queries
HIGH: 3 issues found
--------------------
1. XSS Vulnerability
File: /app/views/users/show.cfm
Line: 12
Code: <h1>#user.name#</h1>
Fix: Use htmlEditFormat() or encodeForHTML()
2. Missing CSRF Token
File: /app/views/users/edit.cfm
Line: 8
Fix: Add authenticityToken() to form
3. Outdated Dependency
Package: jackson-databind
Version: 2.9.0 (CVE-2019-12345)
Fix: Update to version 2.14.0 or higher
MEDIUM: 5 issues found
LOW: 12 issues found
Summary:
- Critical: 1
- High: 3
- Medium: 5
- Low: 12
- Total: 21 vulnerabilities
Recommended Actions:
1. Fix all CRITICAL issues immediately
2. Address HIGH issues before deployment
3. Plan remediation for MEDIUM issues
4. Review LOW issues for false positives
wheels security scan --fix
Auto-fixing security issues...
✓ Added htmlEditFormat() to 3 output statements
✓ Added CSRF tokens to 2 forms
✓ Updated .htaccess security headers
✗ Cannot auto-fix: SQL injection (requires manual review)
Fixed 5 of 8 fixable issues
Manual intervention required for 3 issues
wheels security scan --report=security-report.html
wheels security scan --report=security-report.json
# Example GitHub Actions
- name: Security Scan
run: |
wheels security scan --severity=medium --report=json --output=scan.json
if [ $? -ne 0 ]; then
echo "Security vulnerabilities found"
exit 1
fi
#!/bin/bash
wheels security scan --severity=high
if [ $? -ne 0 ]; then
echo "Commit blocked: Security issues detected"
exit 1
fi
wheels watch [options]
includeDirs
Comma-delimited list of directories to watch
controllers,models,views,config,migrator/migrations
excludeFiles
Comma-delimited list of file patterns to ignore
(none)
interval
Interval in seconds to check for changes
1
reload
Reload framework on changes
true
tests
Run tests on changes
false
migrations
Run migrations on schema changes
false
command
Custom command to run on changes
(none)
debounce
Debounce delay in milliseconds
500
wheels watch
wheels watch --tests
wheels watch includeDirs="controllers,models"
wheels watch excludeFiles="*.txt,*.log"
wheels watch --reload --tests --migrations
wheels watch command="wheels test run"
wheels watch interval=2 debounce=1000
wheels watch reload=false --tests
🔄 Wheels Watch Mode
Monitoring files for changes...
Press Ctrl+C to stop watching
✓ Will reload framework on changes
✓ Will run tests on changes
Watching 145 files across 5 directories
📝 Detected changes:
~ /app/models/User.cfc (modified)
+ /app/models/Profile.cfc (new)
🔄 Reloading application...
✅ Application reloaded successfully at 14:32:15
🧪 Running tests...
✅ All actions completed, watching for more changes...
# Terminal 1: Run server
box server start
# Terminal 2: Watch with reload and tests
wheels watch --reload --tests
# Watch backend files and run build command
wheels watch command="npm run build"
# Focus on models and controllers with tests
wheels watch includeDirs="models,controllers" --tests
# Watch for migration changes
wheels watch includeDirs="migrator/migrations" --migrations
wheels db shell [--datasource=<name>] [--environment=<env>] [--web] [--command=<sql>]
wheels db shell --datasource=myapp_dev
wheels db shell --environment=production
wheels db shell --web
wheels db shell --command="SELECT COUNT(*) FROM users"
wheels db shell
wheels db shell --web
wheels db shell
wheels db shell
wheels db shell
wheels db shell
wheels db shell --web
wheels db shell --command="SELECT COUNT(*) FROM users"
wheels db shell --command="SELECT VERSION()"
wheels db shell --datasource=myapp_test
wheels db shell --datasource=myapp_prod --environment=production
-- H2/MySQL
SHOW TABLES;
-- PostgreSQL
\dt
-- SQL Server
SELECT name FROM sys.tables;
-- H2/MySQL
DESCRIBE users;
-- PostgreSQL
\d users
-- SQL Server
sp_help users;
-- Count records
SELECT COUNT(*) FROM users;
-- View recent records
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
-- Check for specific data
SELECT * FROM users WHERE email = '[email protected]';
# macOS
brew install mysql-client
# Ubuntu/Debian
sudo apt-get install mysql-client
# RHEL/CentOS
sudo yum install mysql
# macOS
brew install postgresql
# Ubuntu/Debian
sudo apt-get install postgresql-client
# RHEL/CentOS
sudo yum install postgresql
# macOS
brew install mssql-tools
# Linux
# Follow Microsoft's instructions for your distribution
wheels db schema [options]
format
string
No
"sql"
Output format (text, json, or sql)
--save
boolean
No
false
Save output to file instead of console
file
string
No
-
File path to write schema to (when using --save)
engine
string
No
"default"
Database engine to use
wheels db schema
wheels db schema format=text
wheels db schema --save file=schema.sql
wheels db schema format=json --save file=schema.json
TABLE: USERS
--------------------------------------------------------------------------------
id INTEGER NOT NULL PRIMARY KEY
username VARCHAR(50) NOT NULL
email VARCHAR(150) NOT NULL
created_at TIMESTAMP
updated_at TIMESTAMP
INDEXES:
- UNIQUE INDEX idx_users_email (email)
- INDEX idx_users_username (username)
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(150) NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
{
"tables": {
"users": {
"columns": [
{"name": "id", "type": "INTEGER", "nullable": false, "primaryKey": true},
{"name": "username", "type": "VARCHAR(50)", "nullable": false},
{"name": "email", "type": "VARCHAR(150)", "nullable": false}
],
"indexes": [
{"name": "idx_users_email", "columns": ["email"], "unique": true}
]
}
}
}
# Export schema after migrations
wheels dbmigrate latest
wheels db schema --save file=db/schema.sql
# Commit schema file
git add db/schema.sql
git commit -m "Update database schema"
# Export human-readable schema
wheels db schema format=text --save file=docs/database-schema.txt
# Export as JSON for documentation tools
wheels db schema format=json --save file=docs/database-schema.json
# Backup current schema
wheels db schema format=sql --save file=backups/schema-$(date +%Y%m%d).sql
# Make your changes
wheels dbmigrate latest
# Export new schema
wheels db schema format=sql --save file=db/schema.sql
# View all tables in text format
wheels db schema format=text
# Export for team review
wheels db schema format=text --save file=database-review.txt
# After running migrations
wheels dbmigrate latest
wheels db schema --save file=db/schema.sql
# Add to git
git add db/schema.sql
git commit -m "Update schema after adding user table"
# Before major changes
wheels db schema --save file=db/schema-before-refactor.sql
# After changes
wheels db schema --save file=db/schema-after-refactor.sql
wheels console [options]
# Start interactive console
wheels console
# Start in testing environment
wheels console environment=testing
# Execute single command
wheels console execute="model('User').findAll().recordCount"
# Start in tag mode
wheels console script=false
// Find a user by ID
user = model("User").findByKey(1)
// Update user properties
user.name = "John Doe"
user.email = "[email protected]"
user.save()
// Create new user
newUser = model("User").create(
name="Jane Smith",
email="[email protected]",
password="secure123"
)
// Find users with conditions
activeUsers = model("User").findAll(
where="active=1 AND createdAt >= '#dateAdd('d', -7, now())#'",
order="createdAt DESC"
)
// Delete a user
model("User").deleteByKey(5)
// Text helpers
pluralize("person") // Returns: "people"
singularize("users") // Returns: "user"
capitalize("hello world") // Returns: "Hello World"
// Date helpers
timeAgoInWords(dateAdd('h', -2, now())) // Returns: "2 hours ago"
distanceOfTimeInWords(now(), dateAdd('d', 7, now())) // Returns: "7 days"
// URL helpers
urlFor(route="user", key=1) // Generate URL for user route
linkTo(text="Home", route="root") // Generate link HTML
// Run custom SQL query
results = query("
SELECT u.*, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.userId
GROUP BY u.id
")
// Simple count query
userCount = query("SELECT COUNT(*) as total FROM users").total
// View application settings
application.wheels.environment
application.wheels.dataSourceName
application.wheels.version
// Check loaded models
structKeyArray(application.wheels.models)
// View routes (if available)
application.wheels.routes
wheels server start
wheels console
Create a new Wheels application from templates.
wheels generate app [name] [template] [directory] [options]
wheels g app [name] [template] [directory] [options]
wheels new [name] [template] [directory] [options]
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.
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)
lucee
--useBootstrap
Include Bootstrap CSS
false
--setupH2
Setup H2 embedded database
true
--init
Initialize as CommandBox package
false
--force
Overwrite existing directory
false
--help
Show help information
wheels generate app myapp
Backend Edition template
Complete MVC structure
Sample code and configuration
H2 database setup by default
wheels generate app myapp HelloWorld
Simple "Hello World" example
One controller and view
Great for learning
wheels generate app myapp HelloDynamic
Dynamic content example
Database interaction
Form handling
wheels generate app myapp HelloPages
Static pages example
Layout system
Navigation structure
wheels generate app blog
wheels generate app api Base@BE
wheels generate app name=myapp directory=./projects/
wheels generate app portfolio --useBootstrap
wheels generate app demo --setupH2
wheels generate app name=enterprise template=HelloDynamic directory=./apps/ \
reloadPassword=secret \
datasourceName=enterprise_db \
cfmlEngine=adobe \
--useBootstrap \
--setupH2
myapp/
├── .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 myapp
H2 is setup by default (--setupH2=true)
No external database needed
Perfect for development
Auto-configured datasource
To disable: --setupH2=false
Create application:
wheels generate app myapp datasourceName=myapp_db --setupH2=false
Configure in CommandBox:
server set app.datasources.myapp_db={...}
Navigate to directory
cd myapp
Install dependencies
box install
Start server
box server start
Open browser
http://localhost:3000
Create custom templates in ~/.commandbox/cfml/modules/wheels-cli/templates/apps/
:
mytemplate/
├── config/
├── controllers/
├── models/
├── views/
└── template.json
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
wheels init - Initialize existing application
wheels generate app-wizard - Interactive app creation
wheels scaffold - Generate CRUD scaffolding
Create an empty database migration file with up and down methods.
wheels dbmigrate create blank --name=<name> [options]
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.
--name
Type: String
Required: Yes
Description: The name of the migration (will be prefixed with timestamp)
--datasource
Type: String
Default: Application default
Description: Specify the datasource this migration targets
--description
Type: String
Default: Empty
Description: Add a description comment to the migration file
--template
Type: String
Default: blank
Description: Use a custom template for the migration
wheels dbmigrate create blank --name=add_custom_indexes
wheels dbmigrate create blank --name=update_user_permissions --description="Add role-based permissions to users"
wheels dbmigrate create blank --name=legacy_data_cleanup --datasource=legacyDB
The command creates a file named YYYYMMDDHHmmss_<name>.cfc
with the following structure:
component extends="wheels.migrator.Migration" hint="<description>" {
function up() {
transaction {
// Add your migration code here
}
}
function down() {
transaction {
// Add code to reverse the migration
}
}
}
For complex operations not covered by other generators:
# 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
When you need to migrate data, not just schema:
# Create data migration
wheels dbmigrate create blank --name=normalize_user_emails
# Edit to add data transformation logic
# Example: lowercase all email addresses
For migrations requiring multiple coordinated changes:
# 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
For database-specific features not abstracted by Wheels:
# Create migration for PostgreSQL-specific features
wheels dbmigrate create blank --name=add_json_columns
# Edit to use PostgreSQL JSON operations
Use clear, descriptive names that indicate the migration's purpose:
# Good
wheels dbmigrate create blank --name=add_user_authentication_tokens
# Bad
wheels dbmigrate create blank --name=update1
Always implement both up() and down() methods:
function up() {
transaction {
execute("CREATE INDEX idx_users_email ON users(email)");
}
}
function down() {
transaction {
execute("DROP INDEX idx_users_email");
}
}
Wrap operations in transactions for atomicity:
function up() {
transaction {
// All operations succeed or all fail
createTable("new_table");
execute("INSERT INTO new_table SELECT * FROM old_table");
dropTable("old_table");
}
}
Document complex operations:
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
");
}
}
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, column, options)
- 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
wheels dbmigrate create table
- Create a table migration
wheels dbmigrate create column
- Create a column migration
wheels dbmigrate up
- Run migrations
wheels dbmigrate down
- Rollback migrations
wheels dbmigrate info
- View migration status
Base command for environment management in Wheels applications.
wheels env [subcommand]
The wheels env
command provides environment management for Wheels applications. It handles environment configuration, switching between environments, and managing environment-specific settings.
list
List all configured environments
setup
Setup a new environment
switch
Switch to a different environment
When called without subcommands, displays help information:
wheels env
Output:
🌍 Wheels Environment Management
Available commands:
wheels env list
List all configured environments
wheels env setup <environment>
Setup a new environment (development, staging, production)
Options: --template=docker --database=postgres
wheels env switch <environment>
Switch to a different environment
Examples:
wheels env setup development
wheels env setup production --template=docker --database=postgres
wheels env switch staging
wheels env list
wheels env setup development
wheels env setup production --template=docker --database=postgres
wheels env switch production
Each environment has its own configuration:
/config/
├── development/
│ └── settings.cfm
├── testing/
│ └── settings.cfm
├── production/
│ └── settings.cfm
└── environment.cfm
The command respects these environment variables:
WHEELS_ENV
Current environment
development
WHEELS_DATASOURCE
Database name
Per environment
WHEELS_DEBUG
Debug mode
Per environment
Order of precedence:
Command line argument
WHEELS_ENV
environment variable
.wheels-env
file
Default (development
)
Debug enabled
Detailed error messages
Hot reload active
Development database
Test database
Fixtures loaded
Debug enabled
Isolated from production
Debug disabled
Optimized performance
Production database
Error handling active
Production-like
Separate database
Debug configurable
Pre-production testing
Local environment override:
production
Environment-specific variables:
# .env.production
DATABASE_URL=mysql://prod@host/db
CACHE_ENABLED=true
DEBUG_MODE=false
Many commands respect current environment:
# Uses current environment's database
wheels dbmigrate latest
# Reloads in current environment
wheels reload
# Tests run in test environment
wheels test run
Access current environment:
<cfset currentEnv = get("environment")>
<cfif currentEnv eq "production">
<!--- Production-specific code --->
</cfif>
Never commit .wheels-env
file
Use testing environment for tests
Match staging to production closely
Separate databases per environment
Environment-specific configuration files
Local Development: Switch between feature environments
Testing: Isolated test environment
Deployment: Environment-specific configurations
Debugging: Quick environment switching
Team Development: Consistent environments
Environment changes may require application restart
Database connections are environment-specific
Some settings only take effect after reload
Use version control for environment configs
wheels env setup - Setup new environment
wheels env list - List environments
wheels env switch - Switch environments
wheels config - Configuration management
Show detailed status of the Wheels development server.
wheels server status [options]
The wheels server status
command displays the current status of your CommandBox server along with Wheels-specific information. It provides more context than the standard server status command.
name
Type: String
Description: Name of the server to check
Example: wheels server status name=myapp
--json
Type: Boolean flag
Description: Output status in JSON format
Example: wheels server status --json
--verbose
Type: Boolean flag
Description: Show detailed server information
Example: wheels server status --verbose
# Check default server status
wheels server status
# Check specific server
wheels server status name=myapp
# Get JSON output for scripting
wheels server status --json
# Show verbose details
wheels server status --verbose
When the server is running, displays:
Server running status
URL and port information
Wheels framework version
Application root directory
Quick action suggestions
Example output:
Wheels Server Status
===================
Status: Running
URL: http://127.0.0.1:60000
PID: 12345
Wheels Application Info:
Wheels Version: 2.5.0
Application Root: /Users/you/myapp
Quick Actions:
wheels server open - Open in browser
wheels server log - View logs
wheels reload - Reload application
The command enhances CommandBox's native status with Wheels-specific information
Use --json
format when integrating with scripts or other tools
The verbose flag shows additional server configuration details
wheels server start
- Start the server
wheels server stop
- Stop the server
wheels server log
- View server logs
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 the root of 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.
CFML: app/views/say/goodbye.cfm
<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.
Welcome 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:
- Essential commands like init, reload, watch
- Generate applications, models, controllers, views
- Complete database management and migrations
- Run tests and generate coverage
- 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
- Understand the CLI's architecture
- Extend the CLI with your own commands
- Customize code generation templates
- Write and run tests effectively
- Database migration best practices
- Security scanning and hardening
- Optimization techniques
- All available configuration settings
- Variables available in templates
- Understanding command exit codes
- Environment configuration
Generate complete applications or individual components:
Complete database lifecycle management:
Comprehensive testing support:
Enhance your development workflow:
Install CommandBox (if not already installed):
Install Wheels CLI:
Create Your First App:
Explore Commands:
Documentation:
GitHub:
Slack: - #wheels channel
Forums:
We welcome contributions! See our 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
- Complete command reference
- Get started in minutes
- Extend the CLI
- Technical deep dive
- Testing best practices
Wheels CLI is open source software licensed under the Apache License 2.0. See for details.
Ready to get started? Head to the or explore the .
Get up and running with Wheels CLI in minutes.
CommandBox 5.0+
Java 8+
Database (MySQL, PostgreSQL, SQL Server, or H2)
This creates a new Wheels application with:
Complete directory structure
Configuration files
Sample code
Edit /config/settings.cfm
:
Or use H2 embedded database:
Create the database:
Visit http://localhost:3000
Let's create a blog post feature:
This generates:
Model with validations
Controller with CRUD actions
Views for all actions
Database migration
Test files
Edit /config/routes.cfm
:
Visit http://localhost:3000/posts
You now have a fully functional blog post management system!
In a new terminal:
Now changes to .cfc
and .cfm
files trigger automatic reloads.
Let's add comments to posts:
Always use migrations for database changes:
Generate tests for your code:
Add to .gitignore
:
In /config/settings.cfm
:
Port already in use:
Database connection failed:
Migration failed:
Need to reset database:
Access database directly:
Read the Guides:
Explore Commands:
wheels --help
wheels generate --help
wheels dbmigrate --help
Join the Community:
#wheels channel
Here's a complete blog setup:
You now have a working blog with posts, authors, and comments!
Generate code coverage reports for your test suite.
The wheels test coverage
command runs your test suite while collecting code coverage metrics. It generates detailed reports showing which parts of your code are tested and identifies areas that need more test coverage.
Instruments Code: Adds coverage tracking to your application
Runs Tests: Executes all specified tests
Collects Metrics: Tracks which lines are executed
Generates Reports: Creates coverage reports in requested formats
Analyzes Results: Provides insights and recommendations
Percentage of code lines executed:
Percentage of functions tested:
Percentage of code branches tested:
Percentage of statements executed:
Interactive web-based report:
Features:
File browser
Source code viewer
Line-by-line coverage
Sortable metrics
Trend charts
Machine-readable format:
For CI/CD integration:
Compatible with:
Jenkins
GitLab CI
GitHub Actions
SonarQube
Quick terminal output:
Configure in .wheels-coverage.json
:
.wheels-coverage.json
:
.git/hooks/pre-push
:
The HTML report highlights:
Red: Uncovered lines
Yellow: Partially covered branches
Green: Fully covered code
Critical Paths: Ensure high coverage
Complex Logic: Test all branches
Error Handling: Cover edge cases
New Features: Maintain coverage
Set Realistic Goals: Start with achievable thresholds
Incremental Improvement: Gradually increase thresholds
Focus on Quality: 100% coverage doesn't mean bug-free
Test Business Logic: Prioritize critical code
Regular Monitoring: Track coverage trends
Coverage collection adds overhead:
Slower test execution
Increased memory usage
Larger test artifacts
Tips:
Run coverage in CI/CD, not every test run
Use incremental coverage for faster feedback
Exclude third-party code
Check if tests are actually running
Verify include/exclude patterns
Look for untested files
Ensure code is instrumented
Check file path patterns
Verify test execution
Check output directory permissions
Verify report format support
Review error logs
Coverage data is collected during test execution
Some code may be unreachable and shouldn't count
Focus on meaningful coverage, not just percentages
Different metrics provide different insights
- Run tests
- Run specific tests
- Debug test execution
Display or switch the current Wheels environment.
The wheels environment
command manages your Wheels application environment settings. It can display the current environment, switch between environments, and list all available environments. Environment changes can trigger automatic application reloads to ensure your settings take effect immediately.
action
Type: String
Default: show
Options: show
, set
, list
Description: Action to perform
Example: wheels environment set production
value
Type: String
Description: Environment value when using set action
Options: development
, testing
, production
, maintenance
Example: wheels environment set development
--reload
Type: Boolean
Default: true
Description: Reload application after changing environment
Example: wheels environment set production --reload=false
Description: Development mode with debugging enabled, no caching
Use for: Local development and debugging
Features:
Debug information displayed
Error details shown
No query caching
No view caching
Hot reloading enabled
Description: Testing mode for running automated tests
Use for: Running test suites and CI/CD pipelines
Features:
Consistent test environment
Test database connections
Predictable caching behavior
Error details available
Description: Production mode with caching enabled, debugging disabled
Use for: Live production servers
Features:
Query caching enabled
View caching enabled
Debug information hidden
Optimized performance
Error pages for users
Description: Maintenance mode to show maintenance page
Use for: During deployments or maintenance windows
Features:
Maintenance page displayed
Admin access still available
Database migrations possible
Public access restricted
Configuration Storage: Environment settings are stored in:
.env
file (WHEELS_ENV variable)
Environment variables
Application configuration
Runtime Detection: The environment is determined by:
Checking server environment variables
Reading .env
file
Defaulting to development
Change Process:
Updates .env
file
Optionally reloads application
Changes take effect immediately (if reloaded)
Create or modify .env
in your project root:
Set system environment variable:
System environment variables (highest priority)
.env
file
Default (development)
Development Workflow
Use development
for local work
Switch to testing
before running tests
Never use development
in production
Production Deployment
Always use production
environment
Set via environment variables for security
Disable reload after deployment
Testing Strategy
Use testing
for automated tests
Ensure consistent test environment
Reset between test runs
Maintenance Windows
Switch to maintenance
during deployments
Provide clear maintenance messages
Switch back to production
when complete
Check if server needs restart: wheels server restart
Verify .env
file permissions
Check for system environment variable conflicts
Some environment checks require running server
Start server first: wheels server start
File-based changes work without server
Ensure write access to .env
file
Check directory permissions
Run with appropriate user privileges
Don't commit .env
files with production settings
Use environment variables in production
Restrict access to environment commands in production
Log environment changes for audit trails
- Reload application
- Restart server
- Test in different environments
# 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 index
# 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 migrations
# 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 reports
# Watch for file changes
wheels watch
# Reload application
wheels reload development
# Analyze code
wheels analyze code
wheels security scan
# macOS/Linux
curl -fsSl https://downloads.ortussolutions.com/debs/gpg | sudo apt-key add -
or
brew install commandbox
# Windows
choco install commandbox
box install wheels-cli
wheels new myapp
cd myapp
box server start
wheels --help
wheels generate --help
wheels dbmigrate --help
3.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+
# 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 commandbox
box install wheels-cli
wheels new blog
cd blog
<cfset set(dataSourceName="blog_development")>
wheels new blog --setupH2
# If using external database (MySQL, PostgreSQL, etc.)
wheels db create
box server start
wheels scaffold name=post properties=title:string,content:text,published:boolean
wheels dbmigrate latest
<cfscript>
// Add this line
resources("posts");
</cfscript>
wheels reload
wheels watch
# Run all tests
wheels test run
# Watch mode
wheels test run --watch
# Specific tests
wheels test run tests/models/PostTest.cfc
# Generate comment model
wheels generate model comment --properties="author:string,content:text,postId:integer" \
--belongs-to="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.cfm
# Create tables
wheels dbmigrate create table products
# Add columns
wheels dbmigrate create column products featured
# Create indexes
wheels dbmigrate create blank add_index_to_products
# 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 production
git init
git add .
git commit -m "Initial Wheels application"
/db/sql/
/logs/
/temp/
.env
tail -f logs/wheels.log
<cfset set(showDebugInformation=true)>
box server start port=3001
# Check datasource
box server info
box server show
# Check status
wheels db status
# Run specific migration
wheels dbmigrate exec 20240120000000
# Or rollback and try again
wheels db rollback
# Complete reset (careful - destroys all data!)
wheels db reset --force
# CLI shell
wheels db shell
# Web console (H2 only)
wheels db shell --web
# 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 \
--belongs-to=post
# Update associations
wheels generate property post authorId:integer --belongs-to=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
wheels watch
# Visit http://localhost:3000/posts
wheels test coverage [options]
--type
Type of tests to run: app, core, or plugin
app
--servername
Name of server to reload
(current server)
--reload
Force a reload of wheels
false
--debug
Show debug info
false
--output-dir
Directory to output the coverage report
tests/coverageReport
wheels test coverage
wheels test coverage --type=core
wheels test coverage --output-dir=reports/coverage
wheels test coverage --reload --debug
wheels test coverage --servername=myapp
File: /app/models/User.cfc
Lines: 156/200 (78%)
Functions: 45/50 (90%)
Branches: 120/150 (80%)
Statements: 890/1000 (89%)
wheels test coverage --format=html
wheels test coverage --format=json
{
"summary": {
"lines": { "total": 1000, "covered": 850, "percent": 85 },
"functions": { "total": 100, "covered": 92, "percent": 92 },
"branches": { "total": 200, "covered": 160, "percent": 80 }
},
"files": {
"/app/models/User.cfc": {
"lines": { "total": 200, "covered": 156, "percent": 78 }
}
}
}
wheels test coverage --format=xml
wheels test coverage --format=console
Code Coverage Report
===================
Overall Coverage: 85.3%
File Lines Funcs Branch Stmt
---------------------------- -------- -------- -------- --------
/app/models/User.cfc 78.0% 85.0% 72.0% 80.0%
/app/models/Order.cfc 92.0% 95.0% 88.0% 90.0%
/app/controllers/Users.cfc 85.0% 90.0% 82.0% 86.0%
Uncovered Files:
- /app/models/Legacy.cfc (0%)
- /app/helpers/Deprecated.cfc (0%)
wheels test coverage --threshold=80
{
"thresholds": {
"global": 80,
"lines": 85,
"functions": 90,
"branches": 75,
"statements": 85
}
}
{
"thresholds": {
"global": 80,
"files": {
"/app/models/User.cfc": 90,
"/app/models/Order.cfc": 95
}
}
}
{
"include": [
"app/models/**/*.cfc",
"app/controllers/**/*.cfc"
],
"exclude": [
"app/models/Legacy.cfc",
"**/*Test.cfc"
],
"reporters": ["html", "json"],
"reportDir": "./coverage",
"thresholds": {
"global": 80
},
"watermarks": {
"lines": [50, 80],
"functions": [50, 80],
"branches": [50, 80],
"statements": [50, 80]
}
}
- name: Run tests with coverage
run: |
wheels test coverage --format=xml --threshold=80 --fail-on-low
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage.xml
#!/bin/bash
wheels test coverage --threshold=80 --fail-on-low
wheels test coverage --format=badge > coverage-badge.svg
# Coverage for changed files only
wheels test coverage --since=HEAD~1
# Generate trend data
wheels test coverage --save-baseline
# Compare with baseline
wheels test coverage --compare-baseline
# From multiple test runs
wheels test coverage --merge coverage1.json coverage2.json
wheels environment [action] [value] [options]
# Show current environment
wheels environment
# Set environment to production
wheels environment set production
# Set environment without reload
wheels environment set development --reload=false
# List all available environments
wheels environment list
# Shortcut syntax - if action matches an environment name
wheels environment development # Same as: wheels environment set development
wheels environment production # Same as: wheels environment set production
wheels environment testing # Same as: wheels environment set testing
Current Wheels Environment
=========================
Environment: development
Wheels Version: 2.5.0
Data Source: myapp_dev
Server: Lucee 5.3.10.120
Environment Settings:
cacheQueries: false
cachePartials: false
cachePages: false
cacheActions: false
showDebugInformation: true
showErrorInformation: true
Available Wheels Environments
============================
development (current)
Description: Development mode with debugging enabled, no caching
Use for: Local development and debugging
testing
Description: Testing mode for running automated tests
Use for: Running test suites and CI/CD pipelines
production
Description: Production mode with caching enabled, debugging disabled
Use for: Live production servers
maintenance
Description: Maintenance mode to show maintenance page
Use for: During deployments or maintenance windows
WHEELS_ENV=production
export WHEELS_ENV=production
Run all pending database migrations to bring database to latest version.
wheels dbmigrate latest
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
╔═══════════════════════════════════════════════╗
║ Running Pending Migrations ║
╚═══════════════════════════════════════════════╝
Current Version: 20240110090000
Target Version: 20240125160000
Migrating...
→ Running 20240115120000_create_orders_table.cfc
Creating table: orders
Adding indexes...
✓ Success (0.125s)
→ Running 20240120140000_add_status_to_orders.cfc
Adding column: status to orders
✓ Success (0.089s)
→ Running 20240125160000_create_categories_table.cfc
Creating table: categories
Adding foreign keys...
✓ Success (0.143s)
╔═══════════════════════════════════════════════╗
║ Migration Complete ║
╚═══════════════════════════════════════════════╝
Previous Version: 20240110090000
Current Version: 20240125160000
Migrations Run: 3
Total Time: 0.357s
Each migration file must contain:
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
}
}
}
Migrations run within transactions:
All changes in a migration succeed or fail together
Database remains consistent
Failed migrations can be retried
function up() {
transaction {
t = createTable("products");
t.string("name");
t.decimal("price");
t.timestamps();
t.create();
}
}
function up() {
transaction {
addColumn(table="users", column="email", type="string");
}
}
function up() {
transaction {
addIndex(table="users", columns="email", unique=true);
}
}
function up() {
transaction {
changeColumn(table="products", column="price", type="decimal", precision=10, scale=2);
}
}
If a migration fails:
→ Running 20240120140000_add_status_to_orders.cfc
Adding column: status to orders
✗ ERROR: Column 'status' already exists
Migration failed at version 20240115120000
Error: Column 'status' already exists in table 'orders'
To retry: Fix the migration file and run 'wheels dbmigrate latest' again
To skip: Run 'wheels dbmigrate up' to run one at a time
Test migrations locally first
# Test on development database
wheels dbmigrate latest
# Verify
wheels dbmigrate info
Backup before production migrations
# Backup database
mysqldump myapp_production > backup.sql
# Run migrations
wheels dbmigrate latest
Use transactions
function up() {
transaction {
// All changes here
}
}
Make migrations reversible
function down() {
transaction {
dropTable("products");
}
}
Migrations can check environment:
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]')");
}
}
}
Preview migrations before running:
# Check what would run
wheels dbmigrate info
# Review migration files
ls app/migrator/migrations/
For large tables:
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");
}
}
}
Add to CI/CD pipeline:
# .github/workflows/deploy.yml
- name: Run migrations
run: |
wheels dbmigrate latest
wheels test app
If issues occur after migration:
Use down migrations
wheels dbmigrate down
wheels dbmigrate down
Restore from backup
mysql myapp_production < backup.sql
Fix and retry
Fix migration file
Run wheels dbmigrate latest
function 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 dbmigrate info - Check migration status
wheels dbmigrate up - Run single migration
wheels dbmigrate down - Rollback migration
wheels dbmigrate create blank - Create migration
List all available environments for your Wheels application.
wheels env list [options]
The wheels env list
command displays all configured environments in your Wheels application. It shows environment details, current active environment, and configuration status.
--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 list
wheels env list --verbose
wheels env list --format=json
wheels env list --check
wheels env list --filter=production
Available Environments
=====================
NAME TYPE DATABASE STATUS
development * Development wheels_dev ✓ Active
testing Testing wheels_test ✓ Valid
staging Staging wheels_staging ✓ Valid
production Production wheels_prod ✓ Valid
qa Custom wheels_qa ⚠ Issues
* = Current environment
Available 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
}
✓ Valid
- Configuration is valid and working
✓ Active
- Currently active environment
⚠ Issues
- Configuration issues detected
✗ 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
# 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*"
wheels env list --sort=name
wheels env list --sort=type
wheels env list --sort=modified
# Get current environment
current=$(wheels env list --format=json | jq -r '.current')
# List all environment names
wheels env list --format=json | jq -r '.environments[].name'
# Verify environment exists
if wheels env list | grep -q "staging"; then
wheels env switch staging
fi
When using --verbose
, shows:
Configuration:
Config file path
Last modified date
File size
Database:
Database name
Datasource name
Connection status
Settings:
Debug mode
Cache settings
Custom configurations
Validation:
Syntax check
Connection test
Dependencies
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
# Export all environments
wheels env list --format=json > environments.json
# Export for documentation
wheels env list --format=markdown > ENVIRONMENTS.md
# Compare environments
wheels env list --compare=development,production
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
wheels env - Environment management overview
wheels env setup - Setup new environment
wheels env switch - Switch environments
wheels config list - List configuration
Execute a script file in the Wheels application context.
wheels runner <file> [options]
The wheels runner
command executes CFML script files with full access to your Wheels application context. This is useful for running data migrations, maintenance tasks, batch processing, and other scripts that need access to your models and application functionality.
Scripts are executed on the server via HTTP, ensuring they run with the proper application context, database connections, and Wheels framework features.
file
Type: String
Required: Yes
Description: Path to the script file to execute (.cfm, .cfc, or .cfs)
Example: wheels runner scripts/migrate-data.cfm
environment
Type: String
Default: development
Description: Environment to run in (development, testing, production)
Example: wheels runner script.cfm environment=production
--verbose
Type: Boolean flag
Default: false
Description: Show detailed output including execution time
Example: wheels runner script.cfm --verbose
params
Type: String (JSON format)
Description: Additional parameters to pass to the script
Example: wheels runner import.cfm params='{"source":"data.csv","limit":100}'
# Run a migration script
wheels runner scripts/migrate-users.cfm
# Run in production environment
wheels runner scripts/cleanup.cfm environment=production
# Run with parameters
wheels runner scripts/import.cfm params='{"file":"products.csv","dryRun":true}'
# Run with verbose output
wheels runner scripts/process-orders.cfm --verbose
<!--- scripts/migrate-legacy-users.cfm --->
<cfscript>
// Access passed parameters
var dryRun = structKeyExists(request.scriptParams, "dryRun") ? request.scriptParams.dryRun : false;
var batchSize = structKeyExists(request.scriptParams, "batchSize") ? request.scriptParams.batchSize : 100;
writeOutput("<h3>Legacy User Migration</h3>");
writeOutput("Dry run: #dryRun#<br>");
writeOutput("Batch size: #batchSize#<br><br>");
// Query legacy database
var legacyUsers = request.query("
SELECT * FROM legacy_users
WHERE migrated = 0
LIMIT #batchSize#
");
writeOutput("Found #legacyUsers.recordCount# users to migrate<br><br>");
var migrated = 0;
for (var legacyUser in legacyUsers) {
try {
if (!dryRun) {
// Create user in new system
var newUser = request.model("User").create(
email: legacyUser.email_address,
firstName: legacyUser.fname,
lastName: legacyUser.lname,
createdAt: legacyUser.created_date
);
// Mark as migrated
request.query("
UPDATE legacy_users
SET migrated = 1, new_id = #newUser.id#
WHERE id = #legacyUser.id#
");
}
migrated++;
writeOutput("✓ Migrated: #legacyUser.email_address#<br>");
} catch (any e) {
writeOutput("✗ Failed: #legacyUser.email_address# - #e.message#<br>");
}
}
writeOutput("<br><strong>Migration complete: #migrated# users processed</strong>");
</cfscript>
<!--- scripts/cleanup-temp-files.cfm --->
<cfscript>
var daysOld = structKeyExists(request.scriptParams, "days") ? request.scriptParams.days : 7;
var verbose = request.scriptVerbose;
writeOutput("Cleaning temporary files older than #daysOld# days...<br><br>");
// Clean up old session files
var cutoffDate = dateAdd('d', -daysOld, now());
var deletedCount = 0;
// Find and delete old upload records
var oldUploads = request.model("TempUpload").findAll(
where="createdAt < '#cutoffDate#'"
);
for (var upload in oldUploads) {
// Delete physical file
if (fileExists(upload.filePath)) {
fileDelete(upload.filePath);
if (verbose) {
writeOutput("Deleted file: #upload.filePath#<br>");
}
}
// Delete database record
request.model("TempUpload").deleteByKey(upload.id);
deletedCount++;
}
writeOutput("<br>Deleted #deletedCount# temporary files<br>");
// Clean up orphaned cart items
var orphanedItems = request.query("
SELECT COUNT(*) as total
FROM cart_items
WHERE updatedAt < '#cutoffDate#'
AND orderId IS NULL
").total;
if (orphanedItems > 0) {
request.query("
DELETE FROM cart_items
WHERE updatedAt < '#cutoffDate#'
AND orderId IS NULL
");
writeOutput("Removed #orphanedItems# orphaned cart items<br>");
}
writeOutput("<br><strong>Cleanup complete!</strong>");
</cfscript>
<!--- scripts/generate-monthly-report.cfm --->
<cfscript>
// Get report parameters
var month = structKeyExists(request.scriptParams, "month") ? request.scriptParams.month : month(now());
var year = structKeyExists(request.scriptParams, "year") ? request.scriptParams.year : year(now());
var emailTo = structKeyExists(request.scriptParams, "emailTo") ? request.scriptParams.emailTo : "";
writeOutput("<h2>Monthly Report - #monthAsString(month)# #year#</h2>");
// Calculate date range
var startDate = createDate(year, month, 1);
var endDate = dateAdd('m', 1, startDate);
// Get statistics
var stats = {};
// New users
stats.newUsers = request.model("User").count(
where="createdAt >= '#startDate#' AND createdAt < '#endDate#'"
);
// Orders
var orderData = request.query("
SELECT
COUNT(*) as totalOrders,
SUM(total) as revenue,
AVG(total) as avgOrderValue
FROM orders
WHERE createdAt >= '#startDate#'
AND createdAt < '#endDate#'
AND status = 'completed'
");
stats.orders = orderData.totalOrders;
stats.revenue = orderData.revenue ?: 0;
stats.avgOrderValue = orderData.avgOrderValue ?: 0;
// Display report
writeOutput("<h3>Summary</h3>");
writeOutput("<ul>");
writeOutput("<li>New Users: #stats.newUsers#</li>");
writeOutput("<li>Total Orders: #stats.orders#</li>");
writeOutput("<li>Revenue: #dollarFormat(stats.revenue)#</li>");
writeOutput("<li>Average Order Value: #dollarFormat(stats.avgOrderValue)#</li>");
writeOutput("</ul>");
// Send email if requested
if (len(emailTo)) {
// Use Wheels email functionality
writeOutput("<br>Sending report to: #emailTo#...<br>");
// Email sending logic here
}
writeOutput("<br><em>Report generated on #dateTimeFormat(now())#</em>");
</cfscript>
Scripts executed by the runner have access to:
request.model(name)
- Access Wheels models
request.query(sql)
- Execute SQL queries
request.scriptParams
- Access passed parameters
request.scriptVerbose
- Check if verbose mode is enabled
All Wheels helper functions
Application scope
Standard CFML functions
<cfscript>
// Access parameters
var limit = structKeyExists(request.scriptParams, "limit") ? request.scriptParams.limit : 10;
// Use models
var users = request.model("User").findAll(maxRows=limit);
// Direct queries
var stats = request.query("SELECT COUNT(*) as total FROM posts");
// Use helpers
writeOutput("Found #pluralize(users.recordCount, 'user')#<br>");
</cfscript>
Error Handling: Always include try/catch blocks for critical operations
Dry Run Mode: Implement a dryRun parameter for destructive operations
Progress Output: Use writeOutput() to show progress for long-running scripts
Transactions: Use database transactions for data consistency
Logging: Log important operations for audit trails
Idempotency: Make scripts safe to run multiple times
Scripts run with full application permissions
Validate and sanitize any external input
Be cautious with production environment scripts
Consider implementing confirmation prompts for destructive operations
Check the file path is correct
Use relative paths from the application root
Ensure file extension is .cfm, .cfc, or .cfs
Start the server: wheels server start
Check server status: wheels server status
Check syntax in your script file
Verify model and table names
Test queries separately in console first
wheels console
- Interactive REPL for testing code
wheels environment
- Manage environment settings
wheels generate migration
- Create database migrations
Base command for code analysis and quality checks.
wheels analyze [subcommand] [options]
The wheels analyze
command provides comprehensive code analysis tools for Wheels applications. It helps identify code quality issues, performance bottlenecks, security vulnerabilities, and provides actionable insights for improvement.
code
Analyze code quality and patterns
performance
Analyze performance characteristics
security
Security vulnerability analysis (deprecated)
--help
Show help information
--version
Show version information
When called without subcommands, runs all analyses:
wheels analyze
This executes:
Code quality analysis
Performance analysis
Security scanning (if not deprecated)
wheels analyze
wheels analyze --summary
wheels analyze --path=./models
wheels analyze --report=html --output=./analysis-report
The analyze command examines:
Coding standards compliance
Code complexity metrics
Duplication detection
Best practices adherence
N+1 query detection
Slow query identification
Memory usage patterns
Cache effectiveness
SQL injection risks
XSS vulnerabilities
Insecure configurations
Outdated dependencies
Wheels Code Analysis Report
==========================
Code Quality
------------
✓ Files analyzed: 234
✓ Total lines: 12,456
⚠ Issues found: 23
- High priority: 3
- Medium priority: 12
- Low priority: 8
Performance
-----------
✓ Queries analyzed: 156
⚠ Potential N+1 queries: 4
⚠ Slow queries detected: 2
✓ Cache hit ratio: 87%
Security (Deprecated)
--------------------
! Security analysis has been deprecated
! Use 'wheels security scan' instead
Summary Score: B+ (82/100)
Configure via .wheels-analysis.json
:
{
"analyze": {
"exclude": [
"vendor/**",
"tests/**",
"*.min.js"
],
"rules": {
"complexity": {
"maxComplexity": 10,
"maxDepth": 4
},
"duplication": {
"minLines": 5,
"threshold": 0.05
}
},
"performance": {
"slowQueryThreshold": 1000,
"cacheTargetRatio": 0.8
}
}
}
- name: Run code analysis
run: |
wheels analyze --format=json --output=analysis.json
wheels analyze --format=badge > analysis-badge.svg
Set minimum scores:
wheels analyze --min-score=80 --fail-on-issues=high
wheels analyze --report=html
Interactive dashboard
Detailed issue breakdown
Code snippets with issues
wheels analyze --format=json
Machine-readable format
CI/CD integration
Custom processing
wheels analyze --format=markdown
Documentation-friendly
Pull request comments
Wiki integration
CFScript best practices
SQL query optimization
Security patterns
Memory management
Create custom rules in .wheels-analysis-rules/
:
module.exports = {
name: "custom-rule",
check: function(file, content) {
// Rule implementation
}
};
Track improvement over time:
# Create baseline
wheels analyze --save-baseline
# Compare with baseline
wheels analyze --compare-baseline
// wheels-analyze-ignore-next-line
complexQuery = ormExecuteQuery(sql, params);
/* wheels-analyze-ignore-start */
// Complex code block
/* wheels-analyze-ignore-end */
{
"ignore": [
{
"rule": "sql-injection",
"file": "legacy/*.cfc"
}
]
}
Incremental Analysis: Analyze only changed files
Parallel Processing: Use multiple cores
Cache Results: Reuse analysis for unchanged files
Focused Scans: Target specific directories
Pre-commit Hooks: Catch issues before commit
Pull Request Checks: Automated code review
Technical Debt: Track and reduce over time
Team Standards: Enforce coding guidelines
Performance Monitoring: Identify bottlenecks
Run analysis regularly
Fix high-priority issues first
Set realistic quality gates
Track metrics over time
Integrate with development workflow
Exclude vendor directories
Use incremental mode
Increase memory allocation
Tune rule sensitivity
Add specific ignores
Update rule definitions
First run may take longer due to initial scanning
Results are cached for performance
Some rules require database connection
Memory usage scales with codebase size
wheels analyze code - Code quality analysis
wheels analyze performance - Performance analysis
wheels security scan - Security scanning
wheels test - Run tests
Generate a migration file for adding columns to an existing database table.
wheels dbmigrate create column name=<table_name> data-type=<type> column-name=<column> [options]
Alias: wheels db create column
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.
name
string
Yes
-
The name of the database table to modify
data-type
string
Yes
-
The column type to add
column-name
string
No
-
The column name to add
default
any
No
-
The default value to set for the column
--null
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
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:
[timestamp]_create_column_[columnname]_in_[tablename]_table.cfc
Example:
20240125160000_create_column_email_in_user_table.cfc
wheels dbmigrate create column name=user data-type=string column-name=email
wheels dbmigrate create column name=user data-type=boolean column-name=is_active default=true
wheels dbmigrate create column name=user data-type=string column-name=bio --null=true limit=500
wheels dbmigrate create column name=product data-type=decimal column-name=price precision=10 scale=2
For the command:
wheels dbmigrate create column name=user data-type=string column-name=phone --null=true
Generates:
component extends="wheels.migrator.Migration" hint="create column phone in user table" {
function up() {
transaction {
addColumn(table="user", columnType="string", columnName="phone", null=true);
}
}
function down() {
transaction {
removeColumn(table="user", column="phone");
}
}
}
Add preference column to user table:
# Create separate migrations for each column
wheels dbmigrate create column name=user data-type=boolean column-name=newsletter_subscribed default=true
wheels dbmigrate create column name=user data-type=string column-name=theme_preference default="light"
Add tracking column to any table:
wheels dbmigrate create column name=product data-type=integer column-name=last_modified_by --null=true
wheels dbmigrate create column name=product data-type=datetime column-name=last_modified_at --null=true
Add decimal columns for pricing:
wheels dbmigrate create column name=product data-type=decimal column-name=price precision=10 scale=2 default=0
wheels dbmigrate create column name=product data-type=decimal column-name=cost precision=10 scale=2
For existing tables with data, make new columns nullable or provide defaults:
# Good - nullable
wheels dbmigrate create column name=user data-type=text column-name=bio --null=true
# Good - with default
wheels dbmigrate create column name=user data-type=string column-name=status default="active"
# Bad - will fail if table has data (not nullable, no default)
wheels dbmigrate create column name=user data-type=string column-name=required_field --null=false
Choose the right column type for your data:
# For short text
wheels dbmigrate create column name=user data-type=string column-name=username limit=50
# For long text
wheels dbmigrate create column name=post data-type=text column-name=content
# For money
wheels dbmigrate create column name=invoice data-type=decimal column-name=amount precision=10 scale=2
This command creates one column at a time:
# Create separate migrations for related columns
wheels dbmigrate create column name=customer data-type=string column-name=address_line1
wheels dbmigrate create column name=customer data-type=string column-name=city
wheels dbmigrate create column name=customer data-type=string column-name=state limit=2
Think through column requirements before creating:
Data type and size
Null constraints
Default values
Index requirements
Add foreign key columns with appropriate types:
# Add foreign key column
wheels dbmigrate create column name=order data-type=integer column-name=customer_id
# Then create index in separate migration
wheels dbmigrate create blank name=add_order_customer_id_index
For special column types, use blank migrations:
# 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 data-type=string column-name=required_field --null=false
# Do this instead
wheels dbmigrate create column name=user data-type=string column-name=required_field default="pending"
This command adds columns, not modifies them:
# Wrong - trying to change existing column type
wheels dbmigrate create column name=user data-type=integer column-name=age
# Right - use blank migration for modifications
wheels dbmigrate create blank name=change_user_age_to_integer
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
wheels dbmigrate create table
- Create new tables
wheels dbmigrate create blank
- Create custom migrations
wheels dbmigrate remove table
- Remove tables
wheels dbmigrate up
- Run migrations
wheels dbmigrate down
- Rollback migrations
Generate a controller with actions and optional views.
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.
Creates:
/controllers/Products.cfc
with index
action
/views/products/index.cfm
Creates controller with all CRUD actions and corresponding views.
Automatically generates all RESTful actions:
index
- List all products
show
- Show single product
new
- New product form
create
- Create product
edit
- Edit product form
update
- Update product
delete
- Delete product
Creates:
/controllers/api/Products.cfc
with JSON responses
No view files
Views are automatically generated for non-API controllers:
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
:
Generate tests alongside controllers:
Use plural names for resource controllers
Keep controllers focused on single resources
Use --rest
for standard CRUD operations
Implement proper error handling
Add authentication in init()
method
Use filters for common functionality
- Generate models
- Generate views
- Generate complete CRUD
- Generate controller tests
Setup a new environment configuration for your Wheels application.
The wheels env setup
command creates and configures new environments for your Wheels application. It generates environment-specific configuration files, database settings, and initializes the environment structure.
Creates Configuration Directory:
Generates Settings File:
Database configuration
Environment-specific settings
Debug and cache options
Security settings
Sets Up Database (unless skipped):
Creates database
Configures datasource
Tests connection
Updates Environment Registry:
Adds to available environments
Sets up environment detection
Example config/staging/settings.cfm
:
Full debugging
No caching
Detailed errors
Hot reload
Test database
Debug enabled
Isolated data
Fast cleanup
Production-like
Some debugging
Performance testing
Pre-production validation
No debugging
Full caching
Error handling
Optimized performance
Create specialized environments:
The command sets up support for:
Environments can inherit settings:
After setup, the command validates:
Configuration file syntax
Database connectivity
Directory permissions
Environment detection
Configure how environment is detected:
Naming Convention: Use clear, consistent names
Base Selection: Choose appropriate base environment
Security: Use strong reload passwords
Documentation: Document environment purposes
Testing: Test configuration before use
Check database permissions
Verify connection settings
Use --skip-database
and create manually
Check syntax in settings.cfm
Verify file permissions
Review error logs
Check environment.cfm logic
Verify server variables
Test detection rules
Multi-Stage Pipeline: dev → staging → production
Feature Testing: Isolated feature environments
Performance Testing: Dedicated performance environment
Client Demos: Separate demo environments
A/B Testing: Multiple production variants
Environment names should be lowercase
Avoid spaces in environment names
Each environment needs unique database
Restart application after setup
Test thoroughly before using
- Environment management overview
- List environments
- Switch environments
- Configuration management
Switch to a different environment in your Wheels application.
The wheels env switch
command changes the active environment for your Wheels application. It updates configuration files, environment variables, and optionally restarts services to apply the new environment settings.
Validates Target Environment:
Checks if environment exists
Verifies configuration
Tests database connection
Updates Configuration:
Sets WHEELS_ENV variable
Updates .wheels-env file
Modifies environment.cfm if needed
Applies Changes:
Clears caches
Reloads configuration
Restarts services (if requested)
Verifies Switch:
Confirms environment active
Checks application health
Reports status
Before:
After:
Updates system environment:
Before switching, validates:
Configuration:
File exists
Syntax valid
Required settings present
Database:
Connection works
Tables accessible
Migrations current
Dependencies:
Required services available
File permissions correct
Resources accessible
Full validation
Graceful transition
Rollback on error
Skip validation
Immediate switch
Use with caution
Prepare new environment
Switch load balancer
No service interruption
Restarts:
Application server
Cache services
Background workers
Configure in .wheels-cli.json
:
If switch fails or causes issues:
Check validation errors
Verify target environment exists
Use --force
if necessary
Check service status
Review error logs
Manually restart services
Verify credentials
Check network access
Test connection manually
Always Validate: Don't skip checks in production
Use Backups: Enable backup for critical switches
Test First: Switch in staging before production
Monitor After: Check application health post-switch
Document Changes: Log environment switches
Production switches require confirmation
Sensitive configs protected
Audit trail maintained
Access controls enforced
Some changes require application restart
Database connections may need reset
Cached data cleared on switch
Background jobs may need restart
- Environment management overview
- List environments
- Setup environments
- Reload application
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; offers an excellent open source alternative. Using 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:
2018 / 2021 / 2023
5.2.1.9+ / 6
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 .
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 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+
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.
wheels generate controller [name] [actions] [options]
wheels g controller [name] [actions] [options]
name
Name of the controller to create (usually plural)
Required
actions
Actions to generate (comma-delimited, default: CRUD for REST)
--rest
Generate RESTful controller with CRUD actions
false
--api
Generate API controller (no view-related actions)
false
description
Controller description
--force
Overwrite existing files
false
wheels generate controller products
wheels generate controller products actions="index,show,new,create,edit,update,delete"
wheels generate controller products --rest
wheels generate controller api/products --api
wheels generate controller reports actions="dashboard,monthly,yearly,export"
component extends="Controller" {
function init() {
// Constructor
}
function index() {
products = model("Product").findAll();
}
}
component extends="Controller" {
function init() {
// Constructor
}
function index() {
products = model("Product").findAll();
}
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 created successfully");
redirectTo(action="index");
} else {
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 updated successfully");
redirectTo(action="index");
} else {
renderView(action="edit");
}
}
function delete() {
product = model("Product").findByKey(params.key);
if (IsObject(product) && product.delete()) {
flashInsert(success="Product deleted successfully");
} else {
flashInsert(error="Could not delete product");
}
redirectTo(action="index");
}
}
component extends="Controller" {
function init() {
provides("json");
}
function index() {
products = model("Product").findAll();
renderWith(products);
}
function show() {
product = model("Product").findByKey(params.key);
if (IsObject(product)) {
renderWith(product);
} else {
renderWith({error: "Product not found"}, status=404);
}
}
function create() {
product = model("Product").new(params.product);
if (product.save()) {
renderWith(product, status=201);
} else {
renderWith({errors: product.allErrors()}, status=422);
}
}
function update() {
product = model("Product").findByKey(params.key);
if (IsObject(product) && product.update(params.product)) {
renderWith(product);
} else {
renderWith({errors: product.allErrors()}, status=422);
}
}
function delete() {
product = model("Product").findByKey(params.key);
if (IsObject(product) && product.delete()) {
renderWith({message: "Product deleted"});
} else {
renderWith({error: "Could not delete"}, status=400);
}
}
}
<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>
<cfset get(name="products", to="products##index")>
<cfset get(name="product", to="products##show")>
<cfset post(name="products", to="products##create")>
<cfset resources("products")>
<cfset namespace("api")>
<cfset resources("products")>
</cfset>
wheels generate controller products --rest
wheels generate test controller products
function init() {
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 env setup [name] [options]
name
Environment name (e.g., staging, qa, production)
Required
--base
Base environment to copy from
development
--database
Database name
wheels_[name]
--datasource
CF datasource name
wheels_[name]
--debug
Enable debug mode
false
--cache
Enable caching
Based on name
--reload-password
Password for reload
Random
--skip-database
Skip database creation
false
--force
Overwrite existing environment
false
--help
Show help information
wheels env setup staging
wheels env setup qa --database=wheels_qa_db --datasource=qa_datasource
wheels env setup staging --base=production
wheels env setup production --debug=false --cache=true --reload-password=secret123
wheels env setup testing --skip-database
/config/[environment]/
└── settings.cfm
<cfscript>
// Environment: staging
// Generated: 2024-01-15 10:30:45
// Database settings
set(dataSourceName="wheels_staging");
// Environment settings
set(environment="staging");
set(showDebugInformation=true);
set(showErrorInformation=true);
// Caching
set(cacheFileChecking=false);
set(cacheImages=false);
set(cacheModelInitialization=false);
set(cacheControllerInitialization=false);
set(cacheRoutes=false);
set(cacheActions=false);
set(cachePages=false);
set(cachePartials=false);
set(cacheQueries=false);
// Security
set(reloadPassword="generated_secure_password");
// URLs
set(urlRewriting="partial");
// Custom settings for staging
set(sendEmailOnError=true);
set(errorEmailAddress="[email protected]");
</cfscript>
wheels env setup performance-testing --base=production --cache=false
wheels env setup staging
# Creates: wheels_staging database
# Datasource: wheels_staging
wheels env setup staging \
--database=staging_db \
--datasource=myapp_staging
wheels env setup production \
--database-url="mysql://user:pass@host:3306/db"
# .env.staging
WHEELS_ENV=staging
WHEELS_DATASOURCE=wheels_staging
WHEELS_DEBUG=true
WHEELS_CACHE=false
DATABASE_URL=mysql://localhost/wheels_staging
// config/staging/settings.cfm
<cfinclude template="../production/settings.cfm">
// Override specific settings
set(showDebugInformation=true);
set(cacheQueries=false);
// config/environment.cfm
if (cgi.server_name contains "staging") {
set(environment="staging");
} else if (cgi.server_name contains "qa") {
set(environment="qa");
} else {
set(environment="production");
}
wheels env setup reporting \
--database=wheels_reporting \
--read-database=wheels_replica
wheels env setup production \
--servers=web1,web2,web3 \
--load-balancer=nginx
// In settings.cfm
set(features={
newCheckout: true,
betaAPI: false,
debugToolbar: true
});
# Export existing config
wheels env export production > prod-config.json
# Import to new environment
wheels env setup staging --from-config=prod-config.json
wheels env switch [name] [options]
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
--help
Show help information
wheels env switch staging
wheels env switch production --restart
wheels env switch testing --force
wheels env switch production --backup
wheels env switch development --quiet
Switching environment...
Current: development
Target: staging
✓ Validating staging environment
✓ Configuration valid
✓ Database connection successful
✓ Updating environment settings
✓ Clearing caches
✓ Environment switched successfully
New Environment: staging
Database: wheels_staging
Debug: Enabled
Cache: Partial
development
staging
export WHEELS_ENV=staging
export WHEELS_DATASOURCE=wheels_staging
wheels env switch production
wheels env switch staging --force --no-check
wheels env switch production --strategy=blue-green
wheels env switch production --backup
# Creates: .wheels-env-backup-20240115-103045
wheels env restore --from=.wheels-env-backup-20240115-103045
# If switch fails
cp .wheels-env-backup-20240115-103045 .wheels-env
wheels reload
wheels env switch production --restart
wheels env switch staging --restart-services=app,cache
{
"env": {
"switch": {
"pre": [
"wheels test run --quick",
"git stash"
],
"post": [
"wheels dbmigrate latest",
"wheels cache clear",
"npm run build"
]
}
}
}
wheels env switch production
# Warning: Switching from development to production
# - Debug will be disabled
# - Caching will be enabled
# - Error details will be hidden
# Continue? (y/N)
wheels env switch development
# Warning: Switching from production to development
# - Debug will be enabled
# - Caching will be disabled
# - Sensitive data may be exposed
# Continue? (y/N)
- name: Switch to staging
run: |
wheels env switch staging --check
wheels test run
wheels deploy exec staging
#!/bin/bash
# deploy.sh
# Switch environment
wheels env switch $1 --backup
# Run migrations
wheels dbmigrate latest
# Clear caches
wheels cache clear
# Verify
wheels env | grep $1
# Automatic rollback
wheels env switch production --auto-rollback
# Manual rollback
wheels env switch:rollback
# Force previous environment
wheels env switch development --force
Interactive wizard for creating a new Wheels application.
wheels generate app-wizard [options]
wheels g app-wizard [options]
The wheels generate app-wizard
command provides an interactive, step-by-step wizard for creating a new Wheels application. It guides you through all configuration options with helpful prompts and explanations, making it ideal for beginners or when you want to explore all available options.
--expert
Show advanced options
false
--skip-install
Skip dependency installation
false
--help
Show help information
? What is the name of your application? › myapp
Must be alphanumeric with hyphens/underscores
Used for directory and configuration names
? Which template would you like to use? ›
❯ Base - Minimal Wheels application
Base@BE - Backend-only (no views)
HelloWorld - Simple example application
HelloDynamic - Database-driven example
HelloPages - Static pages example
? Where should the application be created? › ./myapp
Defaults to ./{app-name}
Can specify absolute or relative path
? Would you like to configure a database? (Y/n) › Y
? Database type? ›
❯ H2 (Embedded)
MySQL
PostgreSQL
SQL Server
Custom
? Select additional features: ›
◯ Bootstrap CSS framework
◯ jQuery library
◯ Sample authentication
◯ API documentation
◯ Docker configuration
? Which CFML engine will you use? ›
❯ Lucee 5
Lucee 6
Adobe ColdFusion 2018
Adobe ColdFusion 2021
Adobe ColdFusion 2023
? Set reload password (leave blank for 'wheels'): › ****
? Enable CSRF protection? (Y/n) › Y
? Enable secure cookies? (y/N) › N
Application Configuration:
─────────────────────────
Name: myapp
Template: Base
Directory: ./myapp
Database: H2 (Embedded)
Features: Bootstrap, jQuery
Engine: Lucee 5
Reload PWD: ****
? Create application with these settings? (Y/n) › Y
graph TD
A[Start Wizard] --> B[Enter App Name]
B --> C[Select Template]
C --> D[Choose Directory]
D --> E[Configure Database]
E --> F[Select Features]
F --> G[Choose CFML Engine]
G --> H[Security Settings]
H --> I[Review Configuration]
I --> J{Confirm?}
J -->|Yes| K[Create Application]
J -->|No| B
K --> L[Install Dependencies]
L --> M[Show Next Steps]
Enable expert mode for additional options:
wheels generate app-wizard --expert
Additional prompts in expert mode:
Custom server ports
JVM settings
Environment-specific configurations
Advanced routing options
Custom plugin repositories
Build tool integration
Save and reuse configurations:
? Save this configuration as a profile? (y/N) › Y
? Profile name: › enterprise-api
wheels generate app-wizard profile=enterprise-api
wheels generate app-wizard --list-profiles
Includes Bootstrap 5.x
Responsive grid system
Pre-styled components
Example layouts
Latest jQuery version
AJAX helpers configured
Example usage in views
User model with secure passwords
Login/logout controllers
Session management
Protected routes example
OpenAPI/Swagger setup
Auto-generated documentation
Interactive API explorer
Multi-stage Dockerfile
docker-compose.yml
Development & production configs
Database containers
After successful creation, the wizard displays:
✓ Application created successfully!
Next steps:
1. cd myapp
2. box install (or run manually if skipped)
3. box server start
4. Visit http://localhost:3000
Additional commands:
- wheels test Run tests
- wheels dbmigrate up Run migrations
- wheels generate Generate code
- wheels help Show all commands
The wizard handles common issues:
Invalid names: Suggests valid alternatives
Existing directories: Offers to overwrite or choose new location
Missing dependencies: Provides installation instructions
Configuration errors: Allows editing before creation
Start with letter
Alphanumeric plus -
and _
No spaces or special characters
Not a reserved word
Must be writable
Cannot be system directory
Warns if not empty
Minimum 6 characters
Strength indicator
Confirmation required
Add templates to ~/.wheels/templates/
:
~/.wheels/templates/
├── my-template/
│ ├── template.json
│ ├── config/
│ ├── controllers/
│ └── views/
template.json
:
{
"name": "My Custom Template",
"description": "Custom template for specific use case",
"author": "Your Name",
"version": "1.0.0",
"prompts": [
{
"name": "apiVersion",
"message": "API version?",
"default": "v1"
}
]
}
Generate with CI configuration:
wheels generate app-wizard ci=github
Includes:
.github/workflows/test.yml
Build configuration
Deployment scripts
Generate with IDE files:
wheels generate app-wizard ide=vscode
Includes:
.vscode/settings.json
.vscode/launch.json
.editorconfig
Run wizard in empty directory
Choose descriptive application names
Configure database early
Enable security features for production
Save profiles for team consistency
Review all settings before confirming
Choose Base@BE template
Skip Bootstrap/jQuery
Enable API documentation
Configure CORS settings
Choose Base template
Include Bootstrap/jQuery
Add sample authentication
Configure session management
Choose Base@BE template
Configure Docker
Set specific ports
Minimal dependencies
Check terminal compatibility
Try --no-interactive
mode
Check system resources
Verify internet connection
Check CommandBox version
Try --skip-install
and install manually
Review generated .wheels-cli.json
Check server.json
settings
Verify file permissions
wheels generate app - Non-interactive app generation
wheels init - Initialize existing directory
wheels scaffold - Generate CRUD scaffolding
The Wheels CLI provides comprehensive database management commands that make it easy to create, manage, and maintain your application's database throughout the development lifecycle.
Database management in Wheels is divided into two main categories:
Database Commands (wheels db
) - High-level database operations
Migration Commands (wheels dbmigrate
) - Schema versioning and changes
This guide covers the database management commands. For migration-specific operations, see the migrations guide.
The wheels db create
command creates a new database based on your datasource configuration:
# Create database using default datasource
wheels db create
# Create database for specific datasource
wheels db create --datasource=myapp_dev
# Create database for specific environment
wheels db create --environment=production
Note: The datasource must already be configured in your CFML server admin. The command will create the database itself but not the datasource configuration.
The wheels db drop
command removes an existing database:
# Drop database (with confirmation)
wheels db drop
# Drop database without confirmation
wheels db drop --force
# Drop specific datasource
wheels db drop --datasource=myapp_dev
Warning: This is a destructive operation. Always backup important data before dropping a database.
The wheels db setup
command performs a complete database initialization:
# Full setup: create + migrate + seed
wheels db setup
# Setup without seeding
wheels db setup --skip-seed
# Setup with custom seed count
wheels db setup --seed-count=20
This is ideal for setting up a new development environment or initializing a test database.
The wheels db reset
command completely rebuilds your database:
# Reset database (drop + create + migrate + seed)
wheels db reset
# Reset without confirmation
wheels db reset --force
# Reset without seeding
wheels db reset --skip-seed
# Reset specific environment
wheels db reset --environment=testing
Important: This command will destroy all existing data. Use with caution, especially in production environments.
The wheels db seed
command populates your database with test or sample data:
# Seed with default settings (5 records per model)
wheels db seed
# Seed with custom record count
wheels db seed --count=10
# Seed specific models only
wheels db seed --models=user,post,comment
# Seed from a JSON file
wheels db seed --dataFile=seeds/test-data.json
Example seed data file format:
{
"users": [
{
"name": "John Doe",
"email": "[email protected]",
"role": "admin"
},
{
"name": "Jane Smith",
"email": "[email protected]",
"role": "user"
}
],
"posts": [
{
"title": "Welcome Post",
"content": "This is the first post",
"userId": 1
}
]
}
The wheels db status
command shows the current state of your migrations:
# Show migration status in table format
wheels db status
# Show status in JSON format
wheels db status --format=json
# Show only pending migrations
wheels db status --pending
Output example:
| Version | Description | Status | Applied At |
|---------------------|----------------------------------|----------|-------------------|
| 20231201120000 | CreateUsersTable | applied | 2023-12-01 12:30 |
| 20231202140000 | AddEmailToUsers | applied | 2023-12-02 14:15 |
| 20231203160000 | CreatePostsTable | pending | Not applied |
The wheels db version
command shows the current schema version:
# Show current version
wheels db version
# Show detailed version information
wheels db version --detailed
The wheels db rollback
command reverses previously applied migrations:
# Rollback last migration
wheels db rollback
# Rollback multiple migrations
wheels db rollback --steps=3
# Rollback to specific version
wheels db rollback --target=20231201120000
# Force rollback without confirmation
wheels db rollback --steps=5 --force
The wheels db dump
command exports your database:
# Basic dump (auto-named with timestamp)
wheels db dump
# Dump to specific file
wheels db dump --output=backup.sql
# Dump schema only (no data)
wheels db dump --schema-only
# Dump data only (no schema)
wheels db dump --data-only
# Dump specific tables
wheels db dump --tables=users,posts,comments
# Dump with compression
wheels db dump --output=backup.sql.gz --compress
The wheels db restore
command imports a database dump:
# Restore from SQL file
wheels db restore backup.sql
# Restore compressed file
wheels db restore backup.sql.gz --compressed
# Clean restore (drop existing objects first)
wheels db restore backup.sql --clean
# Force restore without confirmation
wheels db restore backup.sql --force
# 1. Clone the repository
git clone https://github.com/myapp/repo.git
cd repo
# 2. Install dependencies
box install
# 3. Setup database
wheels db setup
# 4. Start the server
server start
# Quick reset with fresh data
wheels db reset --force
# Or manually:
wheels db drop --force
wheels db create
wheels dbmigrate latest
wheels db seed --count=10
# Create timestamped backup
wheels db dump --compress
# Or with custom filename
wheels db dump --output=prod-backup-$(date +%Y%m%d).sql.gz --compress
# Export from development
wheels db dump --output=dev-data.sql --environment=development
# Import to staging
wheels db restore dev-data.sql --environment=staging
The wheels db shell
command provides direct access to your database's interactive shell:
# Launch CLI shell for current datasource
wheels db shell
# Launch web-based console (H2 only)
wheels db shell --web
# Use specific datasource
wheels db shell --datasource=myapp_dev
# Execute single command
wheels db shell --command="SELECT COUNT(*) FROM users"
H2 Database:
# CLI shell
wheels db shell
# Web console (opens in browser)
wheels db shell --web
MySQL:
# Opens mysql client
wheels db shell
# Connects to: mysql -h host -P port -u user -p database
PostgreSQL:
# Opens psql client
wheels db shell
# Connects to: psql -h host -p port -U user -d database
SQL Server:
# Opens sqlcmd client
wheels db shell
# Connects to: sqlcmd -S server -d database -U user
The database shell commands require the appropriate database client tools to be installed:
H2: No additional installation needed (included with Lucee)
MySQL: Install mysql
client
PostgreSQL: Install psql
client
SQL Server: Install sqlcmd
client
The database commands support multiple database engines:
MySQL/MariaDB - Full support for all operations
PostgreSQL - Full support for all operations
SQL Server - Full support for most operations
H2 - Full support (auto-created databases)
Oracle - Limited support (basic operations)
Database commands use the datasource configuration from your Wheels application. You can override settings using command parameters:
# Use specific datasource
wheels db create --datasource=myapp_test
# Use specific environment
wheels db setup --environment=testing
Confirmation Prompts - Destructive operations require confirmation
Force Flag - Use --force
to skip confirmations in scripts
Environment Detection - Extra warnings for production environments
Transaction Support - Operations are wrapped in transactions where possible
Datasource Not Found
Error: Datasource 'myapp' not found in server configuration
Solution: Create the datasource in your CFML server admin first.
Database Already Exists
Error: Database already exists: myapp_dev
Solution: Use wheels db drop
first or use wheels db reset
instead.
Permission Denied
Error: Access denied for user 'myuser'@'localhost'
Solution: Ensure the database user has CREATE/DROP privileges.
Missing Database Tools
Error: mysqldump not found in PATH
Solution: Install database client tools for dump/restore operations.
Always backup before destructive operations
wheels db dump --output=backup-before-reset.sql
wheels db reset
Use environment-specific datasources
myapp_dev
for development
myapp_test
for testing
myapp_prod
for production
Automate with scripts
#!/bin/bash
# reset-db.sh
wheels db dump --output=backups/pre-reset-$(date +%s).sql
wheels db reset --force
echo "Database reset complete"
Version control your seeds
Keep seed files in db/seeds/
Use environment-specific seed files
Document seed data structure
wheels dbmigrate
- Database migration commands
wheels test
- Test database operations
wheels generate model
- Generate models with migrations
Generate and populate test data.
wheels db seed [options]
The db seed
command populates your database with test data. This is useful for development environments, testing scenarios, and demo installations. The command will generate sample data based on your models.
file
string
No
"seed"
Path to seed file (without .cfm extension)
environment
string
No
current
Environment to seed
wheels db seed
wheels db seed file=demo
wheels db seed environment=testing
wheels db seed file=development/users
Seed files should be placed in the db/
directory of your application:
app/
db/
seed.cfm # Default seed file
demo.cfm # Custom seed file
development/ # Environment-specific seeds
users.cfm
products.cfm
db/seed.cfm
)<cfscript>
// db/seed.cfm
// Create admin user
user = model("user").create(
username = "admin",
email = "[email protected]",
password = "password123",
role = "admin"
);
// Create sample categories
categories = [
{name: "Electronics", slug: "electronics"},
{name: "Books", slug: "books"},
{name: "Clothing", slug: "clothing"}
];
for (category in categories) {
model("category").create(category);
}
// Create sample products
electronicsCategory = model("category").findOne(where="slug='electronics'");
products = [
{
name: "Laptop",
price: 999.99,
category_id: electronicsCategory.id,
in_stock: true
},
{
name: "Smartphone",
price: 699.99,
category_id: electronicsCategory.id,
in_stock: true
}
];
for (product in products) {
model("product").create(product);
}
writeOutput("Seed data created successfully!");
</cfscript>
// db/development/users.cfm
<cfscript>
// Admin users
admins = [
{username: "admin", email: "[email protected]", role: "admin"},
{username: "moderator", email: "[email protected]", role: "moderator"}
];
for (admin in admins) {
admin.password = hash("password123");
model("user").create(admin);
}
// Regular users
for (i = 1; i <= 50; i++) {
model("user").create(
username = "user#i#",
email = "user#i#@example.com",
password = hash("password123"),
created_at = dateAdd("d", -randRange(1, 365), now())
);
}
writeOutput("Created admin and sample users");
</cfscript>
Create consistent development data:
# Reset and seed development database
wheels dbmigrate reset
wheels dbmigrate latest
wheels db seed
Prepare test database:
# Seed test environment
wheels db seed environment=testing
# Run tests
wheels test run
Create demonstration data:
# Load demo dataset
wheels db seed file=demo
Organize seeds by purpose:
# Seed users
wheels db seed file=development/users
# Seed products
wheels db seed file=development/products
<cfscript>
// Generate random users
firstNames = ["John", "Jane", "Bob", "Alice", "Charlie", "Diana"];
lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones"];
cities = ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix"];
for (i = 1; i <= 100; i++) {
model("customer").create(
first_name = firstNames[randRange(1, arrayLen(firstNames))],
last_name = lastNames[randRange(1, arrayLen(lastNames))],
email = "customer#i#@example.com",
city = cities[randRange(1, arrayLen(cities))],
created_at = dateAdd("d", -randRange(1, 365), now())
);
}
</cfscript>
<cfscript>
// Create users
users = [];
for (i = 1; i <= 10; i++) {
users.append(model("user").create(
username = "user#i#",
email = "user#i#@example.com"
));
}
// Create posts for each user
for (user in users) {
postCount = randRange(5, 15);
for (j = 1; j <= postCount; j++) {
post = model("post").create(
user_id = user.id,
title = "Post #j# by #user.username#",
content = "This is sample content for the post.",
published_at = dateAdd("d", -randRange(1, 30), now())
);
// Add comments
commentCount = randRange(0, 10);
for (k = 1; k <= commentCount; k++) {
randomUser = users[randRange(1, arrayLen(users))];
model("comment").create(
post_id = post.id,
user_id = randomUser.id,
content = "Comment #k# on post",
created_at = dateAdd("h", k, post.published_at)
);
}
}
}
</cfscript>
<cfscript>
// Only seed if empty
if (model("user").count() == 0) {
// Create initial users
model("user").create(
username = "admin",
email = "[email protected]",
password = "password123"
);
writeOutput("Created initial users<br>");
} else {
writeOutput("Users already exist, skipping<br>");
}
// Environment-specific seeding
if (get("environment") == "development") {
// Add development-specific data
for (i = 1; i <= 10; i++) {
model("user").create(
username = "testuser#i#",
email = "test#i#@example.com"
);
}
writeOutput("Added development test users<br>");
}
</cfscript>
Make seeds safe to run multiple times:
<cfscript>
// Check before creating
if (!model("user").exists(where="username='admin'")) {
model("user").create(
username = "admin",
email = "[email protected]",
password = "password123"
);
writeOutput("Created admin user<br>");
} else {
writeOutput("Admin user already exists<br>");
}
</cfscript>
Wrap seeds in transactions:
<cfscript>
transaction {
try {
// Create users
for (i = 1; i <= 10; i++) {
model("user").create(
username = "user#i#",
email = "user#i#@example.com"
);
}
// Create related data
// ...
writeOutput("Seed completed successfully<br>");
} catch (any e) {
transaction action="rollback";
writeOutput("Error: #e.message#<br>");
rethrow;
}
}
</cfscript>
Structure seeds logically:
app/db/
├── seed.cfm # Default seed file
├── demo.cfm # Demo data
├── development/ # Development seeds
│ ├── users.cfm
│ ├── products.cfm
│ └── orders.cfm
└── testing/ # Test-specific seeds
└── test_data.cfm
Add clear documentation:
<cfscript>
/**
* Seed file: products.cfm
* Creates: 5 categories, 50 products
* Dependencies: None
* Runtime: ~2 seconds
*/
// Create categories first
categories = [...];
// Then create products
// ...
</cfscript>
<cfscript>
try {
user = model("user").create(
username = "testuser",
email = "invalid-email" // Will fail validation
);
if (user.hasErrors()) {
writeOutput("Failed to create user:<br>");
for (error in user.allErrors()) {
writeOutput("- #error.message#<br>");
}
}
} catch (any e) {
writeOutput("Error: #e.message#<br>");
}
</cfscript>
<cfscript>
// Check dependencies
if (model("category").count() == 0) {
writeOutput("ERROR: Categories must be seeded first!<br>");
abort;
}
// Continue with product seeding
// ...
</cfscript>
Seed files must be placed in the app/db/
directory
The file parameter should not include the .cfm extension
Seed files are executed in the application context with access to all models
Output from seed files is displayed to the user
Consider performance when seeding large amounts of data
wheels dbmigrate latest
- Run migrations before seeding
wheels db schema
- Export/import database structure
wheels generate model
- Generate models for seeding
wheels test run
- Test with seeded data
Generate a model with properties, validations, and associations.
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.
Creates:
/models/User.cfc
Migration file (if enabled)
Common validation methods:
Lifecycle callbacks:
When --migration
is enabled:
Naming: Use singular names (User, not Users)
Properties: Define all database columns
Validations: Add comprehensive validations
Associations: Define all relationships
Callbacks: Use for automatic behaviors
Indexes: Add to migration for performance
Generate model tests:
- Create migrations
- Add properties to existing models
- Generate controllers
- Generate complete CRUD
Generate route definitions for your application.
The wheels generate route
command helps you create route definitions in your Wheels application's /config/routes.cfm
file. It can generate individual routes with different HTTP methods, RESTful resources, or root routes.
Generates in /config/routes.cfm
:
Generates:
Generates:
Generates:
Generates:
This creates all standard 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)
Generates:
Generates:
Creates routes like:
/posts/:postKey/comments
/posts/:postKey/comments/:key
Generates:
Generates:
Generates:
Generates:
Generates:
Generates:
Generates:
Generates:
Generates:
/config/routes.cfm
:
Generated routes create URL helpers:
Generates:
Output:
Order matters: Place specific routes before generic ones
Use RESTful routes: Prefer resources()
over individual routes
Name your routes: Always provide names for URL helpers
Group related routes: Use namespaces and modules
Add constraints: Validate dynamic segments
Document complex routes: Add comments explaining purpose
Test route resolution: Ensure routes work as expected
Route caching: Routes are cached in production
Minimize regex: Complex patterns slow routing
Avoid wildcards: Be specific when possible
Order efficiently: Most-used routes first
Check route order
Verify HTTP method
Test with wheels routes test
Check for typos in pattern
Ensure unique route names
Check for duplicate patterns
Use namespaces to avoid conflicts
Verify parameter names match
Check constraint patterns
Test with various inputs
- Generate complete CRUD with routes
- Generate controllers
- Generate RESTful resources
- Generate API resources
⚠️ DEPRECATED: This command is deprecated. Use wheels test run
or the advanced testing commands (wheels test:all
, wheels test:unit
, etc.) instead.
Run Wheels framework tests (core, app, or plugin tests).
The wheels test
command runs the built-in Wheels framework test suite. This is different from wheels test run
which runs your application's TestBox tests. Use this command to verify framework integrity or test Wheels plugins.
Tests Wheels framework functionality
Verifies framework integrity
Useful after framework updates
Runs application-level framework tests
Tests Wheels configuration
Verifies app-specific framework features
Tests installed Wheels plugins
Verifies plugin compatibility
Checks plugin functionality
Validations
Associations
Callbacks
Properties
Calculations
Filters
Caching
Provides/formats
Redirects
Rendering
Helper functions
Form helpers
Asset helpers
Partials
Layouts
Routing
URL rewriting
Request handling
Parameter parsing
In /config/settings.cfm
:
Create separate test database:
Database not configured: Check test datasource
Reload password wrong: Verify settings
Plugin conflicts: Disable plugins and retest
Cache issues: Clear cache and retry
Add tests in /tests/framework/
:
Run with timing:
Monitor slow tests:
Tests run in isolation:
Separate request for each test
Transaction rollback (if enabled)
Clean application state
Run before deployment
Test after framework updates
Verify plugin compatibility
Use CI/CD integration
Keep test database clean
- Run TestBox application tests
- Generate coverage reports
- Debug test execution
- Reload application
Base command for security management and vulnerability scanning.
The wheels security
command provides comprehensive security tools for Wheels applications. It scans for vulnerabilities, checks security configurations, and helps implement security best practices.
When called without subcommands, performs a quick security check:
Output:
SQL injection detection
XSS vulnerability scanning
Path traversal checks
Command injection risks
Security headers
CORS settings
Authentication config
Session management
Vulnerable packages
Outdated libraries
License compliance
Supply chain risks
SSL/TLS configuration
Port exposure
File permissions
Environment secrets
Configure via .wheels-security.json
:
Create .wheels-security-policy.yml
:
.git/hooks/pre-commit
:
The command provides guidance:
Executive summary
Detailed findings
Remediation steps
Compliance status
Trend analysis
Check compliance with standards:
Regular Scans: Schedule automated scans
Fix Quickly: Address high-severity issues immediately
Update Dependencies: Keep libraries current
Security Training: Educate development team
Defense in Depth: Layer security measures
Security scans may take time on large codebases
Some checks require running application
False positives should be documented
Regular updates improve detection accuracy
- Detailed security scanning
- Security analysis (deprecated)
Base command for plugin management in Wheels applications.
The wheels plugins
command provides comprehensive plugin management for Wheels applications. It handles plugin discovery, installation, configuration, and lifecycle management.
When called without subcommands, displays plugin overview:
Output:
Each plugin contains plugin.json
:
Default source for plugins:
Configure additional sources:
Plugins can register custom commands:
Usage:
Options:
prompt
: Ask for each conflict
newest
: Use newest version
oldest
: Keep existing version
Shared across projects:
Location: ~/.wheels/plugins/
Project-specific:
Location: /plugins/
Plugin Not Loading
Dependency Conflicts
Version Incompatibility
Version Lock: Lock plugin versions for production
Test Updates: Test in development first
Backup: Backup before major updates
Documentation: Document custom plugins
Security: Verify plugin sources
Plugins are loaded in dependency order
Some plugins require application restart
Global plugins override project plugins
Plugin conflicts are resolved by load order
- List plugins
- Search for plugins
- Show plugin details
- Install plugins
- Update plugins
- Update all plugins
- Check for updates
- Remove plugins
- Create new plugin
wheels generate model [name] [options]
wheels g model [name] [options]
name
Model name (singular)
Required
--migration
Generate database migration
true
properties
Model properties (format: name:type,name2:type2)
belongs-to
Parent model relationships (comma-separated)
has-many
Child model relationships (comma-separated)
has-one
One-to-one relationships (comma-separated)
primary-key
Primary key column name(s)
id
table-name
Custom database table name
description
Model description
--force
Overwrite existing files
false
wheels generate model user
wheels generate model user properties="firstName:string,lastName:string,email:string,age:integer"
wheels generate model post belongs-to="user" has-many="comments"
wheels generate model setting --migration=false
wheels generate model product \
properties="name:string,price:decimal,stock:integer,active:boolean" \
belongs-to="category,brand" \
has-many="reviews,orderItems"
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", columns="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 user properties="email:string,name:string"
wheels generate test model user
wheels generate route [objectname] [options]
wheels g route [objectname] [options]
objectname
The name of the resource/route to add
Optional
get
Create a GET route (pattern,handler format)
post
Create a POST route (pattern,handler format)
put
Create a PUT route (pattern,handler format)
patch
Create a PATCH route (pattern,handler format)
delete
Create a DELETE route (pattern,handler format)
--resources
Create a resources route
false
root
Create a root route with handler
wheels generate route products
.resources("products")
wheels generate route get="/about,pages#about"
.get(pattern="/about", to="pages#about")
wheels generate route post="/contact,contact#send"
.post(pattern="/contact", to="contact#send")
wheels generate route root="pages#home"
.root(to="pages#home")
wheels generate route products --resource
<cfset resources("products")>
wheels generate route products --api
<cfset resources(name="products", nested=false, except="new,edit")>
wheels generate route comments --resource nested="posts"
<cfset resources("posts")>
<cfset resources("comments")>
</cfset>
wheels generate route "/users/[key]/profile" to="users#profile" name="userProfile"
<cfset get(name="userProfile", pattern="/users/[key]/profile", to="users##profile")>
wheels generate route "/blog/[year]/[month?]/[day?]" to="blog#archive" name="blogArchive"
<cfset get(name="blogArchive", pattern="/blog/[year]/[month?]/[day?]", to="blog##archive")>
wheels generate route "/docs/*" to="documentation#show" name="docs"
<cfset get(name="docs", pattern="/docs/*", to="documentation##show")>
wheels generate route "/users/[id]" to="users#show" constraints="id=[0-9]+"
<cfset get(pattern="/users/[id]", to="users##show", constraints={id="[0-9]+"})>
wheels generate route users --resource namespace="admin"
<cfset namespace("admin")>
<cfset resources("users")>
</cfset>
wheels generate route dashboard --resource namespace="admin" module="backend"
<cfset module("backend")>
<cfset namespace("admin")>
<cfset resources("dashboard")>
</cfset>
</cfset>
wheels generate route comments --resource nested="posts" --shallow
<cfset resources("posts")>
<cfset resources(name="comments", shallow=true)>
</cfset>
wheels generate route "products/[key]/activate" to="products#activate" method="PUT" --member
<cfset resources("products")>
<cfset put(pattern="[key]/activate", to="products##activate", on="member")>
</cfset>
wheels generate route "products/search" to="products#search" --collection
<cfset resources("products")>
<cfset get(pattern="search", to="products##search", on="collection")>
</cfset>
<!---
Routes Configuration
Define your application routes below
--->
<!--- Public routes --->
<cfset get(name="home", pattern="/", to="main##index")>
<cfset get(name="about", pattern="/about", to="pages##about")>
<cfset get(name="contact", pattern="/contact", to="pages##contact")>
<cfset post(name="sendContact", pattern="/contact", to="pages##sendContact")>
<!--- Authentication --->
<cfset get(name="login", pattern="/login", to="sessions##new")>
<cfset post(name="createSession", pattern="/login", to="sessions##create")>
<cfset delete(name="logout", pattern="/logout", to="sessions##delete")>
<!--- Resources --->
<cfset resources("products")>
<cfset resources("categories")>
<!--- API routes --->
<cfset namespace("api")>
<cfset namespace("v1")>
<cfset resources(name="products", nested=false, except="new,edit")>
<cfset resources(name="users", nested=false, except="new,edit")>
</cfset>
</cfset>
<!--- Admin routes --->
<cfset namespace("admin")>
<cfset get(name="adminDashboard", pattern="/", to="dashboard##index")>
<cfset resources("users")>
<cfset resources("products")>
<cfset resources("orders")>
</cfset>
<!--- Catch-all route --->
<cfset get(pattern="*", to="errors##notFound")>
<!--- For route: get(name="about", pattern="/about", to="pages##about") --->
#linkTo(route="about", text="About Us")#
#urlFor(route="about")#
#redirectTo(route="about")#
<!--- For route: resources("products") --->
#linkTo(route="products", text="All Products")# <!--- /products --->
#linkTo(route="product", key=product.id, text="View")# <!--- /products/123 --->
#linkTo(route="newProduct", text="Add Product")# <!--- /products/new --->
#linkTo(route="editProduct", key=product.id, text="Edit")# <!--- /products/123/edit --->
#urlFor(route="products")# <!--- /products --->
#urlFor(route="product", key=123)# <!--- /products/123 --->
<!--- For nested resources("posts") > resources("comments") --->
#linkTo(route="postComments", postKey=post.id, text="Comments")# <!--- /posts/1/comments --->
#linkTo(route="postComment", postKey=post.id, key=comment.id, text="View")# <!--- /posts/1/comments/5 --->
wheels generate route "/posts/[year]/[month]" to="posts#archive" constraints="year=[0-9]{4},month=[0-9]{2}"
wheels generate route "/api/users" to="api/users#index" format="json"
<cfset get(pattern="/api/users", to="api/users##index", format="json")>
wheels generate route products --resource
wheels generate test routes products
component extends="wheels.Test" {
function test_products_routes() {
// Test index route
result = $resolve(path="/products", method="GET");
assert(result.controller == "products");
assert(result.action == "index");
// Test show route
result = $resolve(path="/products/123", method="GET");
assert(result.controller == "products");
assert(result.action == "show");
assert(result.params.key == "123");
// Test create route
result = $resolve(path="/products", method="POST");
assert(result.controller == "products");
assert(result.action == "create");
}
}
wheels routes list
wheels routes test "/products/123" --method=GET
Route resolved:
Controller: products
Action: show
Params: {key: "123"}
Name: product
<!--- Public routes --->
<cfset get(name="home", pattern="/", to="main##index")>
<!--- Authenticated routes --->
<cfset namespace(name="authenticated", path="/app")>
<!--- All routes here require authentication --->
<cfset resources("projects")>
<cfset resources("tasks")>
</cfset>
<cfset namespace("api")>
<cfset namespace(name="v1", path="/v1")>
<cfset resources(name="users", except="new,edit")>
</cfset>
<cfset namespace(name="v2", path="/v2")>
<cfset resources(name="users", except="new,edit")>
</cfset>
</cfset>
<cfset subdomain("api")>
<cfset resources("products")>
</cfset>
<cfset subdomain("admin")>
<cfset resources("users")>
</cfset>
<cfset get(pattern="/old-about", redirect="/about")>
<cfset get(pattern="/products/category/[name]", redirect="/categories/[name]")>
wheels test [type] [servername] [options]
type
Test type: core
, app
, or plugin
app
serverName
CommandBox server name
Current server
reload
Reload before running tests
true
debug
Show debug output
false
format
Output format
json
adapter
Test adapter
""
(empty)
--help
Show help information
wheels test core
wheels test app
wheels test plugin
wheels test
wheels test core
wheels test type=app serverName=myserver
wheels test debug=true
wheels test reload=false
⚠️ WARNING: The 'wheels test' command is deprecated.
Please use 'wheels test run' instead.
See: wheels help test run
╔═══════════════════════════════════════════════╗
║ Running Wheels Tests ║
╚═══════════════════════════════════════════════╝
Test Type: app
Server: default
Reloading: Yes
Initializing test environment...
✓ Environment ready
Running tests...
Model Tests
✓ validations work correctly (15ms)
✓ associations load properly (23ms)
✓ callbacks execute in order (8ms)
Controller Tests
✓ filters apply correctly (12ms)
✓ caching works as expected (45ms)
✓ provides correct formats (5ms)
View Tests
✓ helpers render correctly (18ms)
✓ partials include properly (9ms)
✓ layouts apply correctly (11ms)
Plugin Tests
✓ DBMigrate plugin loads (7ms)
✓ Scaffold plugin works (22ms)
╔═══════════════════════════════════════════════╗
║ Test Summary ║
╚═══════════════════════════════════════════════╝
Total Tests: 11
Passed: 11
Failed: 0
Errors: 0
Time: 173ms
✓ All tests passed!
<cfset set(testEnvironment=true)>
<cfset set(testDataSource="myapp_test")>
CREATE DATABASE myapp_test;
wheels test debug=true
Failed: Model Tests > validations work correctly
File: /tests/framework/model/validations.cfc
Line: 45
Expected: true
Actual: false
- name: Run Wheels tests
run: |
box install
box server start
wheels test core
wheels test app
stage('Framework Tests') {
steps {
sh 'wheels test core'
sh 'wheels test app'
}
}
component extends="wheels.Test" {
function test_custom_framework_feature() {
// Test custom framework modification
actual = customFrameworkMethod();
assert(actual == expected);
}
}
wheels test --debug | grep "Time:"
✓ complex query test (523ms) ⚠️ SLOW
✓ simple validation (8ms)
# Check server is running
box server status
# Verify test URL
curl http://localhost:3000/wheels/tests
# Manual reload first
wheels reload
# Then run tests
wheels test reload=false
# Increase heap size
box server set jvm.heapSize=512
box server restart
wheels test app
wheels test core myserver
wheels test debug=true
wheels test run
wheels test run --group=core
wheels test run --verbose
Feature
wheels test
(deprecated)
wheels test run
Purpose
Framework tests
Application tests
Framework
Wheels Test
TestBox
Location
/wheels/tests/
/tests/
Use Case
Framework integrity
App functionality
Status
Deprecated
Current
wheels security [subcommand] [options]
scan
Scan for security vulnerabilities
--help
Show help information
--version
Show version information
wheels security
Wheels Security Overview
=======================
Last Scan: 2024-01-15 10:30:45
Status: 3 issues found
Critical: 0
High: 1
Medium: 2
Low: 0
Vulnerabilities:
- [HIGH] SQL Injection risk in UserModel.cfc:45
- [MEDIUM] Missing CSRF protection on /admin routes
- [MEDIUM] Outdated dependency: cfml-jwt (2.1.0 → 3.0.0)
Run 'wheels security scan' for detailed analysis
wheels security
wheels security --status
wheels security --report
wheels security --check=dependencies
{
"security": {
"scanOnCommit": true,
"autoFix": false,
"severity": "medium",
"ignore": [
{
"rule": "sql-injection",
"file": "legacy/*.cfc",
"reason": "Legacy code, sandboxed"
}
],
"checks": {
"dependencies": true,
"code": true,
"configuration": true,
"infrastructure": true
}
}
}
policies:
- name: "No Direct SQL"
description: "Prevent direct SQL execution"
severity: "high"
rules:
- pattern: "queryExecute\\(.*\\$.*\\)"
message: "Use parameterized queries"
- name: "Secure Headers"
description: "Require security headers"
severity: "medium"
headers:
- "X-Frame-Options"
- "X-Content-Type-Options"
- "Content-Security-Policy"
# Check policy compliance
wheels security --check-policy
# Enforce policies (fail on violation)
wheels security --enforce-policy
#!/bin/bash
wheels security scan --severity=high --fail-on-issues
- name: Security scan
run: |
wheels security scan --format=sarif
wheels security --upload-results
{
"wheels.security": {
"realTimeScan": true,
"showInlineWarnings": true
}
}
wheels security headers --check
// Application.cfc
this.securityHeaders = {
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Strict-Transport-Security": "max-age=31536000",
"Content-Security-Policy": "default-src 'self'"
};
wheels security deps
wheels security deps --fix
wheels security licenses --allowed=MIT,Apache-2.0
# Fix auto-fixable issues
wheels security fix
# Fix specific issue types
wheels security fix --type=headers,csrf
Issue: SQL Injection Risk
File: /models/User.cfc:45
Fix: Replace direct SQL with parameterized query
Current:
query = "SELECT * FROM users WHERE id = #arguments.id#";
Suggested:
queryExecute(
"SELECT * FROM users WHERE id = :id",
{ id: arguments.id }
);
# HTML report
wheels security scan --report=html
# JSON report for tools
wheels security scan --format=json
# SARIF for GitHub
wheels security scan --format=sarif
# OWASP Top 10
wheels security compliance --standard=owasp-top10
# PCI DSS
wheels security compliance --standard=pci-dss
# Custom standard
wheels security compliance --standard=./company-standard.yml
# Start monitoring
wheels security monitor --start
# Check monitor status
wheels security monitor --status
# View alerts
wheels security monitor --alerts
{
"monitoring": {
"alerts": {
"email": "[email protected]",
"slack": "https://hooks.slack.com/...",
"severity": "high"
}
}
}
// Vulnerable
query = "SELECT * FROM users WHERE id = #url.id#";
// Secure
queryExecute(
"SELECT * FROM users WHERE id = :id",
{ id: { value: url.id, cfsqltype: "integer" } }
);
// Vulnerable
<cfoutput>#form.userInput#</cfoutput>
// Secure
<cfoutput>#encodeForHTML(form.userInput)#</cfoutput>
# Check for compromise indicators
wheels security incident --check
# Generate incident report
wheels security incident --report
# Enable security lockdown
wheels security lockdown --enable
# Disable after resolution
wheels security lockdown --disable
wheels plugins [subcommand] [options]
list
List installed plugins
search
Search for plugins on ForgeBox
info
Show detailed plugin information
install
Install a plugin
update
Update a specific plugin
update:all
Update all installed plugins
outdated
List plugins with available updates
remove
Remove a plugin
init
Initialize a new plugin project
--help
Show help information
--version
Show version information
wheels plugins
Wheels Plugin Manager
====================
Installed Plugins: 5
├── authentication (v2.1.0) - User authentication system
├── pagination (v1.5.2) - Advanced pagination helpers
├── validation (v3.0.1) - Extended validation rules
├── caching (v2.2.0) - Enhanced caching strategies
└── api-tools (v1.8.3) - RESTful API utilities
Available Updates: 2
- validation: v3.0.1 → v3.1.0
- api-tools: v1.8.3 → v2.0.0
Run 'wheels plugins list' for detailed information
wheels plugins
wheels plugins --check
wheels plugins --update-all
wheels plugins --info
/plugins/
├── authentication/
│ ├── Authentication.cfc
│ ├── config/
│ ├── models/
│ ├── views/
│ └── plugin.json
├── pagination/
└── ...
{
"name": "authentication",
"version": "2.1.0",
"description": "User authentication system",
"author": "Wheels Community",
"wheels": ">=2.0.0",
"dependencies": {
"validation": ">=3.0.0"
}
}
https://www.forgebox.io/type/cfwheels-plugins/
{
"pluginRegistries": [
"https://www.forgebox.io/type/cfwheels-plugins/",
"https://company.com/wheels-plugins/"
]
}
# Search for plugins
wheels plugins search authentication
# Browse categories
wheels plugins browse --category=security
# Install from registry
wheels plugins install authentication
# Install from GitHub
wheels plugins install github:user/wheels-plugin
# Install from file
wheels plugins install ./my-plugin.zip
# Configure plugin
wheels plugins configure authentication
# View configuration
wheels plugins config authentication
# Check for updates
wheels plugins outdated
# Update specific plugin
wheels plugins update authentication
# Update all plugins
wheels plugins update --all
# Generate plugin scaffold
wheels generate plugin my-plugin
# Plugin structure created:
# /plugins/my-plugin/
# ├── MyPlugin.cfc
# ├── plugin.json
# ├── config/
# ├── tests/
# └── README.md
component extends="wheels.Plugin" {
function init() {
this.version = "1.0.0";
this.author = "Your Name";
this.description = "Plugin description";
}
function setup() {
// Plugin initialization
}
function teardown() {
// Plugin cleanup
}
}
{
"plugins": {
"production": ["caching", "monitoring"],
"development": ["debug-toolbar", "profiler"],
"all": ["authentication", "validation"]
}
}
// In environment.cfm
if (get("environment") == "development") {
addPlugin("debug-toolbar");
}
// In plugin
this.commands = {
"auth:create-user": "commands/CreateUser.cfc",
"auth:reset-password": "commands/ResetPassword.cfc"
};
wheels auth:create-user [email protected]
wheels auth:reset-password user123
# Installs plugin and dependencies
wheels plugins install api-tools
# Also installs: validation, serialization
# When conflicts exist
wheels plugins install authentication --resolve=prompt
wheels plugins install authentication --global
wheels plugins install authentication
# Verify plugin signatures
wheels plugins verify authentication
# Install only verified plugins
wheels plugins install authentication --verified-only
{
"pluginPermissions": {
"fileSystem": ["read", "write"],
"network": ["http", "https"],
"database": ["read", "write"]
}
}
wheels plugins diagnose authentication
wheels plugins deps --tree
wheels plugins check-compatibility
wheels plugins cache clear
wheels plugins cache rebuild
Instructions for upgrading Wheels applications
Wheels follows Semantic Versioning (http://semver.org/) so large version changes (e.g, 1.x.x -> 2.x.x
) will most likely contain breaking changes which will require evaluation of your codebase. Minor version changes (e.g, 1.3.x->1.4.x
) will often contain new functionality, but in a backwards-compatible manner, and maintenance releases (e.g 1.4.4 -> 1.4.5
) will just be trying to fix bugs.
Generally speaking, upgrading Wheels is as easy as replacing the wheels
folder, especially for those small maintenance releases: however, there are usually exceptions in minor point releases (i.e, 1.1
to 1.3
required replacing other files outside the wheels
folder). The notes below detail those changes.
Adobe Coldfusion 2016 and below are no longer compatible with Wheels going forward. Consequently, these versions have been removed from the Wheels Internal Test Suites.
Starting with Wheels 3.x, Wirebox will be used as the default dependency injector.
After installing Wheels 3.x, you'll have to run box install
to intall testbox and wirebox in your application as they are not shipped with Wheels but are rather listed in box.json
file as dependencies to be installed.
Added Mappings for the app
, vendor
, wheels
, wirebox
, testbox
and tests
directories.
root.cfm
and rewrite.cfm
have been removed. All the requests are now being redirected only through public/index.cfm
.
A .env
file has been added in the root of the application which adds the H2 database extension for lucee and sets the cfadmin password to commandbox
for both Lucee and Adobe ColdFusion.
Replace the wheels
folder with the new one from the 3.0.0 download.
Move the wheels
folder inside the vendor
folder.
Moved the config
, controllers
, events
, global
, lib
, migrator
, models
, plugins
, snippets
and views
directories inside the app
directory.
Moved the files
, images
, javascripts
, miscellaneous
, stylesheets
directories and Application.cfc
, index.cfm
and urlrewrite.xml
files into the public
folder.
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 JS Confirm and JS Disable 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 addRoute() function has been removed in Wheels 2.0 in favor of a new routing API. See the Routing 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 wildcard() mapper method:
mapper()
.wildcard()
.end();
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 protectsFromForgery() to the config
method.
Add a call to the csrfMetaTags() 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 csrfMetaTags() 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 POST
ing data to the actions that require post
, patch
, and delete
verbs.
See documentation for the CSRF Protection Plugin for more information.
Note: If you had previously installed the CSRF Protection plugin, 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 http://www.domain.com/site-admin/edit-layout.
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).
Debug test execution with detailed diagnostics and troubleshooting tools.
wheels test debug [spec] [options]
The wheels test debug
command provides advanced debugging capabilities for your test suite. It helps identify why tests are failing, diagnose test environment issues, and provides detailed execution traces for troubleshooting complex test problems.
type
Type of tests to run: app, core, or plugin
app
spec
Specific test spec to run (e.g., models.user)
servername
Name of server to reload
(current server)
--reload
Force a reload of wheels (boolean flag)
false
--break-on-failure
Stop test execution on first failure (boolean flag)
true
output-level
Output verbosity: 1=minimal, 2=normal, 3=verbose
3
wheels test debug
wheels test debug spec=models.user
wheels test debug output-level=1
wheels test debug --break-on-failure=false
wheels test debug type=core --reload
wheels test debug --inspect port=9229
wheels test debug slow=500 verbose=2
🔍 Test Debug Session Started
================================
Environment: testing
Debug Level: 1
Test Framework: TestBox 5.0.0
CFML Engine: Lucee 5.3.9.141
Running: UserModelTest.testValidation
Status: RUNNING
[DEBUG] Setting up test case...
[DEBUG] Creating test user instance
[DEBUG] Validating empty user
[DEBUG] Assertion: user.hasErrors() = true ✓
[DEBUG] Test completed in 45ms
With --trace verbose=3
:
🔍 Test Execution Trace
======================
▶ UserModelTest.setup()
└─ [0.5ms] Creating test database transaction
└─ [1.2ms] Loading test fixtures
└─ [0.3ms] Initializing test context
▶ UserModelTest.testValidation()
├─ [0.1ms] var user = model("User").new()
│ └─ [2.1ms] Model instantiation
│ └─ [0.5ms] Property initialization
├─ [0.2ms] user.validate()
│ └─ [5.3ms] Running validations
│ ├─ [1.2ms] Checking required fields
│ ├─ [2.1ms] Email format validation
│ └─ [2.0ms] Custom validations
├─ [0.1ms] expect(user.hasErrors()).toBe(true)
│ └─ [0.3ms] Assertion passed ✓
└─ [0.1ms] Test completed
Total Time: 10.2ms
Memory Used: 2.3MB
With --step
:
▶ Entering step mode for UserModelTest.testLogin
[1] user = model("User").findOne(where="email='[email protected]'")
> (n)ext, (s)tep into, (c)ontinue, (v)ariables, (q)uit: v
Variables:
- arguments: {}
- local: { user: [undefined] }
- this: UserModelTest instance
> n
[2] expect(user.authenticate("password123")).toBe(true)
> v
Variables:
- arguments: {}
- local: { user: User instance {id: 1, email: "[email protected]"} }
> s
[2.1] Entering: user.authenticate("password123")
Parameters: { password: "password123" }
Set breakpoints in code:
// In test file
function testComplexLogic() {
var result = complexCalculation(data);
debugBreak(); // Execution pauses here
expect(result).toBe(expectedValue);
}
Or via command line:
wheels test debug breakpoint=OrderTest.testCalculateTotal:25
With --dump-context
:
Test Context Dump
================
Test: UserModelTest.testPermissions
Phase: Execution
Application Scope:
- wheels.version: 2.5.0
- wheels.environment: testing
- Custom settings: { ... }
Request Scope:
- cgi.request_method: "GET"
- url: { testMethod: "testPermissions" }
Test Data:
- Fixtures loaded: users, roles, permissions
- Test user: { id: 999, email: "[email protected]" }
- Database state: Transaction active
Component State:
- UserModelTest properties: { ... }
- Inherited properties: { ... }
With slow=500
:
⚠️ Slow Tests Detected
=====================
1. OrderModelTest.testLargeOrderProcessing - 2,345ms 🐌
- Database queries: 45 (1,234ms)
- Model operations: 892ms
- Assertions: 219ms
2. UserControllerTest.testBulkImport - 1,567ms 🐌
- File I/O: 623ms
- Validation: 512ms
- Database inserts: 432ms
3. ReportTest.testGenerateYearlyReport - 987ms ⚠️
- Data aggregation: 654ms
- PDF generation: 333ms
wheels test debug --inspect
Connect with Chrome DevTools:
Open Chrome/Edge
Navigate to chrome://inspect
Click "Configure" and add localhost:9229
Click "inspect" on the target
wheels test debug --inspect-brk port=9230
--inspect
: Enable debugging
--inspect-brk
: Break on first line
Custom port for multiple sessions
With --pause-on-failure
:
✗ Test Failed: UserModelTest.testUniqueEmail
Test paused at failure point.
Failure Details:
- Expected: true
- Actual: false
- Location: UserModelTest.cfc:45
Debug Options:
(i) Inspect variables
(s) Show stack trace
(d) Dump database state
(r) Retry test
(c) Continue
(q) Quit
> i
Local Variables:
- user1: User { email: "[email protected]", id: 1 }
- user2: User { email: "[email protected]", errors: ["Email already exists"] }
Stack Trace:
-----------
1. TestBox.expectation.toBe() at TestBox/system/Expectation.cfc:123
2. UserModelTest.testUniqueEmail() at tests/models/UserModelTest.cfc:45
3. TestBox.runTest() at TestBox/system/BaseSpec.cfc:456
4. Model.validate() at wheels/Model.cfc:789
5. Model.validatesUniquenessOf() at wheels/Model.cfc:1234
wheels test debug --replay
Replays last failed tests with debug info:
Replaying 3 failed tests from last run...
1/3 UserModelTest.testValidation
- Original failure: Assertion failed at line 23
- Replay status: PASSED ✓
- Possible flaky test
2/3 OrderControllerTest.testCheckout
- Original failure: Database connection timeout
- Replay status: FAILED ✗
- Consistent failure
.wheels-test-debug.json
:
{
"debug": {
"defaultLevel": 1,
"slowThreshold": 1000,
"breakpoints": [
"UserModelTest.testComplexScenario:45",
"OrderTest.testEdgeCase:78"
],
"trace": {
"includeFramework": false,
"maxDepth": 10
},
"output": {
"colors": true,
"timestamps": true,
"saveToFile": "./debug.log"
}
}
}
# Run only the failing test
wheels test debug UserModelTest.testValidation --trace
# Dump environment and context
wheels test debug --dump-context > test-context.txt
# Interactive debugging
wheels test debug FailingTest --step --pause-on-failure
# Debug working test
wheels test debug WorkingTest --trace > working.log
# Debug failing test
wheels test debug FailingTest --trace > failing.log
# Compare outputs
diff working.log failing.log
Debug test isolation:
wheels test debug --trace verbose=3 | grep -E "(setup|teardown|transaction)"
Debug timing issues:
wheels test debug slow=100 --trace
wheels test debug --dump-context | grep -A 20 "Database state"
Start Simple: Use basic debug before advanced options
Isolate Issues: Debug one test at a time
Use Breakpoints: Strategic breakpoints save time
Check Environment: Ensure test environment is correct
Save Debug Logs: Keep logs for complex issues
Debug mode affects test performance
Some features require specific CFML engine support
Remote debugging requires network access
Verbose output can be overwhelming - filter as needed
wheels test - Run tests normally
wheels test run - Run specific tests
wheels test coverage - Coverage analysis
Generate view files for controllers.
The wheels generate view
command creates view files for controllers. It can generate individual views using templates or create blank view files.
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
Creates: /views/users/show.cfm
with empty content
Creates: /views/users/show.cfm
using the show template
Creates: /views/users/edit.cfm
using the edit template
Creates: /views/users/_form.cfm
using the form partial template
Creates: /views/products/index.cfm
using the index template
Templates are located in:
Partials start with underscore:
_form.cfm
- Form partial
_item.cfm
- List item partial
_sidebar.cfm
- Sidebar partial
Creates: /views/products/index.html
Creates: /views/emails/welcome.txt
Creates: /views/products/index_es.cfm
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
- Generate controllers
- Generate complete CRUD
- Generate view tests
Add properties to existing model files.
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.
biginteger
- Large integer
binary
- Binary data
boolean
- Boolean (true/false)
date
- Date only
datetime
- Date and time
decimal
- Decimal numbers
float
- Floating point
integer
- Integer
string
- Variable character (VARCHAR)
text
- Long text
time
- Time only
timestamp
- Timestamp
uuid
- UUID/GUID
Creates a string/textField property called firstname
on the User model.
Creates a boolean/checkbox property with default value of 0 (false).
Creates a datetime property on the User model.
Before:
After:
Command:
Generated:
Command:
Generated:
Command:
Generated:
When --migrate=true
(default), generates migration:
app/migrator/migrations/[timestamp]_add_properties_to_[model].cfc
:
Based on property type and options:
Add custom validation rules:
Generated:
Generate with callbacks:
Generated:
Generated:
Generated:
Generated:
Generated:
Generated:
The command intelligently adds properties without disrupting:
Existing properties
Current validations
Defined associations
Custom methods
Comments and formatting
Add properties incrementally
Always generate migrations
Include appropriate validations
Use semantic property names
Add indexes for query performance
Consider default values carefully
Document complex properties
After adding properties:
- Generate models
- Create columns
- Generate tests
Run TestBox tests for your application with advanced features.
The wheels test run
command executes your application's TestBox test suite with support for watching, filtering, and various output formats. This is the primary command for running your application tests (as opposed to framework tests).
Standard test directory layout:
Watch mode reruns tests on file changes:
Output:
Colored console output
Shows progress dots
Summary at end
Plain text output
Good for CI systems
No colors
JUnit XML format
For CI integration
Jenkins compatible
Test Anything Protocol
Cross-language format
Run tests in parallel threads:
Benefits:
Faster execution
Better CPU utilization
Finds concurrency issues
Generate coverage reports:
View report:
Create reusable test utilities:
Use labels for fast feedback
Parallel execution
Watch specific directories
Skip slow tests during development
Use beforeEach
/afterEach
Reset global state
Use transactions
Avoid time-dependent tests
Mock external services
Use fixed test data
- Run framework tests
- Generate coverage
- Debug tests
- Generate test files
wheels generate view [objectName] [name] [template]
wheels g view [objectName] [name] [template]
objectName
View path folder (e.g., user)
Required
name
Name of the file to create (e.g., edit)
Required
template
Optional template to use
wheels generate view user show
wheels generate view user show crud/show
wheels generate view user edit crud/edit
wheels generate view user _form crud/_form
wheels generate view product index crud/index
<!--- 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
~/.commandbox/cfml/modules/wheels-cli/templates/views/
├── default/
│ ├── index.cfm
│ ├── show.cfm
│ ├── new.cfm
│ ├── edit.cfm
│ └── _form.cfm
├── bootstrap/
└── custom/
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=html
wheels generate view emails welcome --format=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=es
<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/index
component 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>
<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 property [name] [options]
wheels g property [name] [options]
name
Table name
Required
column-name
Name of column
Required
data-type
Type of column
string
default
Default value for column
--null
Whether to allow null values
limit
Character or integer size limit for column
precision
Precision value for decimal columns
scale
Scale value for decimal columns
propertyName:type:option1:option2
wheels generate property user column-name=firstname
wheels generate property user column-name=isActive data-type=boolean default=0
wheels generate property user column-name=lastloggedin data-type=datetime
wheels generate property product column-name=price data-type=decimal precision=10 scale=2
wheels generate property user "fullName:calculated"
component extends="Model" {
function init() {
// Existing code
}
}
component extends="Model" {
function init() {
// Existing code
// Properties
property(name="email", sql="email");
// Validations
validatesPresenceOf(properties="email");
validatesUniquenessOf(properties="email");
validatesFormatOf(property="email", regEx="^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
}
wheels generate property product "name:string:required description:text price:float:required:default=0.00 inStock:boolean:default=true"
component extends="Model" {
function init() {
// Properties
property(name="name", sql="name");
property(name="description", sql="description");
property(name="price", sql="price", default=0.00);
property(name="inStock", sql="in_stock", default=true);
// Validations
validatesPresenceOf(properties="name,price");
validatesNumericalityOf(property="price", allowBlank=false, greaterThanOrEqualTo=0);
}
}
wheels generate property comment "userId:integer:required:belongsTo=user postId:integer:required:belongsTo=post"
component extends="Model" {
function init() {
// Associations
belongsTo(name="user", foreignKey="userId");
belongsTo(name="post", foreignKey="postId");
// Properties
property(name="userId", sql="user_id");
property(name="postId", sql="post_id");
// Validations
validatesPresenceOf(properties="userId,postId");
}
}
wheels generate property user fullName:calculated --callbacks
component extends="Model" {
function init() {
// Properties
property(name="fullName", sql="", calculated=true);
}
// Calculated property getter
function getFullName() {
return this.firstName & " " & this.lastName;
}
}
component extends="wheels.migrator.Migration" hint="Add properties to product" {
function up() {
transaction {
addColumn(table="products", columnName="sku", columnType="string", limit=50, null=false);
addColumn(table="products", columnName="price", columnType="decimal", precision=10, scale=2, null=false, default=0.00);
addColumn(table="products", columnName="stock", columnType="integer", null=true, default=0);
addIndex(table="products", columnNames="sku", unique=true);
}
}
function down() {
transaction {
removeIndex(table="products", columnNames="sku");
removeColumn(table="products", columnName="stock");
removeColumn(table="products", columnName="price");
removeColumn(table="products", columnName="sku");
}
}
}
string:required
validatesPresenceOf, validatesLengthOf
string:unique
validatesUniquenessOf
email
validatesFormatOf with email regex
integer
validatesNumericalityOf(onlyInteger=true)
float
validatesNumericalityOf
boolean
validatesInclusionOf(list="true,false,0,1")
date
validatesFormatOf with date pattern
wheels generate property user "age:integer:min=18:max=120"
validatesNumericalityOf(property="age", greaterThanOrEqualTo=18, lessThanOrEqualTo=120);
wheels generate property user lastLoginAt:datetime --callbacks
function init() {
// Properties
property(name="lastLoginAt", sql="last_login_at");
// Callbacks
beforeUpdate("updateLastLoginAt");
}
private function updateLastLoginAt() {
if (hasChanged("lastLoginAt")) {
// Custom logic here
}
}
wheels generate property order "status:string:default=pending:inclusion=pending,processing,shipped,delivered"
property(name="status", sql="status", default="pending");
validatesInclusionOf(property="status", list="pending,processing,shipped,delivered");
wheels generate property user "avatar:string:fileField"
property(name="avatar", sql="avatar");
// In the init() method
afterSave("processAvatarUpload");
beforeDelete("deleteAvatarFile");
private function processAvatarUpload() {
if (hasChanged("avatar") && isUploadedFile("avatar")) {
// Handle file upload
}
}
wheels generate property user "preferences:text:json"
property(name="preferences", sql="preferences");
function getPreferences() {
if (isJSON(this.preferences)) {
return deserializeJSON(this.preferences);
}
return {};
}
function setPreferences(required struct value) {
this.preferences = serializeJSON(arguments.value);
}
wheels generate property user "ssn:string:encrypted"
property(name="ssn", sql="ssn");
beforeSave("encryptSSN");
afterFind("decryptSSN");
private function encryptSSN() {
if (hasChanged("ssn") && Len(this.ssn)) {
this.ssn = encrypt(this.ssn, application.encryptionKey);
}
}
private function decryptSSN() {
if (Len(this.ssn)) {
this.ssn = decrypt(this.ssn, application.encryptionKey);
}
}
wheels generate property post "slug:string:unique:fromProperty=title"
property(name="slug", sql="slug");
validatesUniquenessOf(property="slug");
beforeValidation("generateSlug");
private function generateSlug() {
if (!Len(this.slug) && Len(this.title)) {
this.slug = createSlug(this.title);
}
}
private function createSlug(required string text) {
return reReplace(
lCase(trim(arguments.text)),
"[^a-z0-9]+",
"-",
"all"
);
}
wheels generate property user "
profile.bio:text
profile.website:string
profile.twitter:string
profile.github:string
" --nested
wheels generate property post "publishedAt:timestamp deletedAt:timestamp:nullable"
wheels generate property user email:string
> Property 'email' already exists. Options:
> 1. Skip this property
> 2. Update existing property
> 3. Add with different name
> Choice:
wheels generate property model deletedAt:timestamp:nullable
wheels generate property document "version:integer:default=1 versionedAt:timestamp"
wheels generate property order "status:string:default=pending statusChangedAt:timestamp"
wheels generate property model "createdBy:integer:belongsTo=user updatedBy:integer:belongsTo=user"
# Run migration
wheels dbmigrate latest
# Generate property tests
wheels generate test model user
# Run tests
wheels test
wheels test run [spec] [options]
filter
Filter tests by pattern or name
group
Run specific test group
--coverage
Generate coverage report (boolean flag)
false
reporter
Test reporter format: console, junit, json, tap
console
--watch
Watch for file changes and rerun tests (boolean flag)
false
--verbose
Verbose output (boolean flag)
false
--fail-fast
Stop on first test failure (boolean flag)
false
wheels test run
wheels test run filter="User"
wheels test run filter="test_user_validation"
wheels test run --watch
wheels test run group="unit"
wheels test run group="integration"
wheels test run --coverage
wheels test run reporter=json
wheels test run reporter=junit
wheels test run reporter=tap
wheels test run --fail-fast
wheels test run --verbose --coverage reporter=console
/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 helpers
component extends="testbox.system.BaseSpec" {
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="testbox.system.BaseSpec" {
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,
reporter = "simple",
labels = "",
options = {}
};
}
wheels test run --watch
[TestBox Watch] Monitoring for changes...
[TestBox Watch] Watching: /tests, /models, /controllers
[14:23:45] Change detected: models/User.cfc
[14:23:45] Running tests...
✓ User Model > validates required fields (12ms)
✓ User Model > saves with valid data (45ms)
✓ User Model > prevents duplicate emails (23ms)
Tests: 3 passed, 0 failed
Time: 80ms
[TestBox Watch] Waiting for changes...
wheels test run reporter=simple
wheels test run reporter=text
wheels test run reporter=json
{
"totalDuration": 523,
"totalSpecs": 25,
"totalPass": 24,
"totalFail": 1,
"totalError": 0,
"totalSkipped": 0
}
wheels test run reporter=junit outputFile=results.xml
wheels test run reporter=tap
# Run only model tests
wheels test run bundles=models
# Run multiple bundles
wheels test run bundles=models,controllers
it("can authenticate", function() {
// test code
}).labels("auth,critical");
# Run only critical tests
wheels test run labels=critical
# 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*"
# Skip slow tests
wheels test run excludes="*slow*,*integration*"
wheels test run threads=4
wheels test run --coverage coverageOutputDir=coverage/
open coverage/index.html
// /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 reporter=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
wheels test run labels=unit # Fast
wheels test run labels=integration # Slow
wheels test run threads=4
wheels test run tests/models --watch
wheels test run excludes="*integration*"
# Increase memory
box server set jvm.heapSize=1024
box server restart
Generate complete CRUD scaffolding for a resource.
wheels scaffold name=[resourceName] [options]
The 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.
name
Resource name (singular)
Required
properties
Model properties (format: name:type,name2:type2)
belongs-to
Parent model relationships (comma-separated)
has-many
Child model relationships (comma-separated)
--api
Generate API-only scaffold (no views)
false
--tests
Generate test files
true
--migrate
Run migrations after scaffolding
false
--force
Overwrite existing files
false
wheels scaffold name=product
wheels scaffold name=product properties=name:string,price:decimal,stock:integer
wheels scaffold name=order properties=total:decimal,status:string \
belongsTo=user hasMany=orderItems
wheels scaffold name=product api=true properties=name:string,price:decimal
wheels scaffold name=category properties=name:string migrate=true
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 name=product properties=name:string,price:decimal,stock:integer
:
/models/Product.cfc
component 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.cfc
component 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.cfc
component extends="wheels.migrator.Migration" {
function up() {
transaction {
t = createTable("products");
t.string("name");
t.decimal("price", precision=10, scale=2);
t.integer("stock");
t.timestamps();
t.create();
addIndex(table="products", columns="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 latest
Add routes to /config/routes.cfm
:
<cfset resources("products")>
Restart application:
wheels reload
Test 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 template
Templates 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
Properties: 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
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 products
wheels generate model - Generate models
wheels generate controller - Generate controllers
wheels generate resource - Generate REST resources
wheels dbmigrate latest - Run migrations
Generate a migration file for creating a new database table.
wheels dbmigrate create table name=<table_name> [--force] [--id] primary-key=<key_name>
Alias: wheels db create table
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.
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
primary-key
string
No
"id"
Overrides the default primary key column name
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.
wheels dbmigrate create table name=user
wheels dbmigrate create table name=user_roles --id=false
wheels dbmigrate create table name=products primary-key=productCode
wheels dbmigrate create table name=users --force
For the command:
wheels dbmigrate create table name=users
Generates a migration file that you can customize:
component 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(columnName="name");
// t.integer(columnName="age");
t.timestamps();
t.create();
}
}
function down() {
transaction {
dropTable("users");
}
}
}
Create a typical entity table:
# Generate the migration
wheels dbmigrate create table name=customer
# Then edit the migration file to add columns
Create a join table without primary key:
wheels dbmigrate create table name=products_categories --id=false
Create a table with non-standard primary key:
wheels dbmigrate create table name=legacy_customer primary-key=customer_code
Wheels conventions expect singular table names:
# 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
After generating the migration, edit it to add columns:
// In the generated migration file
t = createTable(name="orders", force=false, id=true, primaryKey="id");
t.integer(columnName="customer_id");
t.decimal(columnName="total", precision=10, scale=2);
t.string(columnName="status", default="pending");
t.timestamps();
t.create();
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:
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");
}
}
}
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
wheels dbmigrate create column
- Add columns to existing table
wheels dbmigrate create blank
- Create custom migration
wheels dbmigrate remove table
- Create table removal migration
wheels dbmigrate up
- Run migrations
wheels dbmigrate info
- View migration status
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:
<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://guides.wheels.dev/2.5.0/v/3.0.0-snapshot/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.
*/
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>
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.
set(dataSourceName="back2thefuture");
// set(dataSourceUserName="marty");
// set(dataSourcePassword="mcfly");
Wheels supports MySQL, SQL Server, PostgreSQL, 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
:
id
int
auto increment
username
varchar(100)
varchar(255)
passwd
varchar(15)
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 conventions 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:
mapper()
.wildcard()
.root(method = "get")
.end();
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:
mapper()
.resources("users")
.wildcard()
.root(method = "get")
.end();
This will create URL endpoints for creating, reading, updating, and deleting user records:
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
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:
<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>
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 startFormTag() function takes parameters similar to the linkTo()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 linkTo().
To end the form, we use the endFormTag() function. Easy enough.
The textField() and passwordField() helpers are similar. As you probably guessed, they create <input>
elements with type="text"
and type="password"
, respectively. And the submitTag() function creates an <input type="submit" />
element.
One thing you'll notice is the textField() and passwordField() 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 model() 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 new() method.
component extends="Controller" {
function config(){}
function new() {
user = model("user").new();
}
}
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/users/new
, we'll see the form with the fields that we defined.
The HTML generated by your application will look something like this:
<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>
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 create() method:
function create() {
user = model("user").create(params.user);
redirectTo(
route="users",
success="User created successfully."
);
}
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 redirectTo() 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:
function index() {
users = model("user").findAll(order="username");
}
This call to the model's findAll() 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
<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>
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 linkTo() form helper function to add an "Edit" button to the form. This action expects a key
as well.
Because in the linkTo() 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:
function edit() {
user = model("user").findByKey(params.key);
}
The view at app/views/users/edit.cfm
looks almost exactly the same as the view for creating a user:
<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>
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:
<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>
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:
function update() {
user = model("user").findByKey(params.key);
user.update(params.user);
redirectTo(
route="editUser",
key=user.id,
success="User updated successfully."
);
}
To update the user
, simply call its update() method with the user
struct passed from the form via params
. It's that simple.
After the update, we'll add a success message using the Flash 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:
function delete() {
user = model("user").findByKey(params.key);
user.delete();
redirectTo(
route="users",
success="User deleted successfully."
);
}
We simply load the user using the model's findByKey() method and then call the object's delete() 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.
Generate a complete RESTful resource with model, controller, views, and routes.
wheels generate resource [name] [properties] [options]
wheels g resource [name] [properties] [options]
The wheels generate resource
command creates a complete RESTful resource including model, controller with all CRUD actions, views, routes, and optionally database migrations and tests. It's a comprehensive generator that sets up everything needed for a functioning resource.
name
Resource name (singular)
Required
--api
Generate API-only resource (no views)
false
--tests
Generate associated tests
true
--migration
Generate database migration
true
belongs-to
Parent model relationships (comma-separated)
has-many
Child model relationships (comma-separated)
attributes
Model attributes (name:type,email:string)
--open
Open generated files
false
--scaffold
Generate with full CRUD operations
true
wheels generate resource product attributes="name:string,price:float,description:text"
Generates:
Model: /models/Product.cfc
Controller: /controllers/Products.cfc
Views: /views/products/
(index, show, new, edit, _form)
Route: resources("products")
in /config/routes.cfm
Migration: /app/migrator/migrations/[timestamp]_create_products.cfc
Tests: /tests/models/ProductTest.cfc
, /tests/controllers/ProductsTest.cfc
wheels generate resource product attributes="name:string,price:float" --api
Generates:
Model: /models/Product.cfc
Controller: /controllers/api/Products.cfc
(JSON responses only)
Route: resources(name="products", except="new,edit")
in API namespace
Migration: /app/migrator/migrations/[timestamp]_create_products.cfc
Tests: API-focused test files
wheels generate resource comment attributes="content:text,approved:boolean" belongs-to="post,user"
Generates nested structure with proper associations and routing.
/models/Product.cfc
:
component extends="Model" {
function init() {
// Properties
property(name="name", sql="name");
property(name="price", sql="price");
property(name="description", sql="description");
// Validations
validatesPresenceOf(properties="name,price");
validatesNumericalityOf(property="price", greaterThan=0);
validatesLengthOf(property="name", maximum=255);
// Callbacks
beforeSave("sanitizeInput");
}
private function sanitizeInput() {
this.name = Trim(this.name);
if (StructKeyExists(this, "description")) {
this.description = Trim(this.description);
}
}
}
/controllers/Products.cfc
:
component extends="Controller" {
function init() {
// Filters
filters(through="findProduct", only="show,edit,update,delete");
}
function index() {
products = model("Product").findAll(order="createdAt DESC");
}
function show() {
// Product loaded by filter
}
function new() {
product = model("Product").new();
}
function create() {
product = model("Product").new(params.product);
if (product.save()) {
flashInsert(success="Product was created successfully.");
redirectTo(route="product", key=product.id);
} else {
renderView(action="new");
}
}
function edit() {
// Product loaded by filter
}
function update() {
if (product.update(params.product)) {
flashInsert(success="Product was updated successfully.");
redirectTo(route="product", key=product.id);
} else {
renderView(action="edit");
}
}
function delete() {
if (product.delete()) {
flashInsert(success="Product was deleted successfully.");
} else {
flashInsert(error="Product could not be deleted.");
}
redirectTo(route="products");
}
// Filters
private function findProduct() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
flashInsert(error="Product not found.");
redirectTo(route="products");
}
}
}
/views/products/index.cfm
:
<h1>Products</h1>
<p>
#linkTo(route="newProduct", text="New Product", class="btn btn-primary")#
</p>
<cfif products.recordCount>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<cfoutput query="products">
<tr>
<td>#linkTo(route="product", key=products.id, text=products.name)#</td>
<td>#dollarFormat(products.price)#</td>
<td>#dateFormat(products.createdAt, "mmm dd, yyyy")#</td>
<td>
#linkTo(route="editProduct", key=products.id, text="Edit", class="btn btn-sm btn-secondary")#
#linkTo(route="product", key=products.id, text="Delete", 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. #linkTo(route="newProduct", text="Create one now")#!</p>
</cfif>
/views/products/_form.cfm
:
#errorMessagesFor("product")#
#startFormTag(route=formRoute, key=formKey, class="needs-validation")#
<div class="mb-3">
#textField(objectName="product", property="name", label="Product Name", class="form-control", required=true)#
</div>
<div class="mb-3">
#numberField(objectName="product", property="price", label="Price", class="form-control", step="0.01", min="0", required=true)#
</div>
<div class="mb-3">
#textArea(objectName="product", property="description", label="Description", class="form-control", rows=5)#
</div>
<div class="mb-3">
#submitTag(value=submitValue, class="btn btn-primary")#
#linkTo(route="products", text="Cancel", class="btn btn-secondary")#
</div>
#endFormTag()#
/app/migrator/migrations/[timestamp]_create_products.cfc
:
component extends="wheels.migrator.Migration" hint="Create products table" {
function up() {
transaction {
createTable(name="products", force=true) {
t.increments("id");
t.string("name", limit=255, null=false);
t.decimal("price", precision=10, scale=2, null=false);
t.text("description");
t.timestamps();
t.index("name");
};
}
}
function down() {
transaction {
dropTable("products");
}
}
}
Added to /config/routes.cfm
:
<cfset resources("products")>
/controllers/api/Products.cfc
:
component extends="Controller" {
function init() {
provides("json");
filters(through="findProduct", only="show,update,delete");
}
function index() {
products = model("Product").findAll(
order="createdAt DESC",
page=params.page ?: 1,
perPage=params.perPage ?: 25
);
renderWith({
data: products,
meta: {
page: products.currentPage,
totalPages: products.totalPages,
totalRecords: products.totalRecords
}
});
}
function show() {
renderWith(product);
}
function create() {
product = model("Product").new(params.product);
if (product.save()) {
renderWith(data=product, status=201);
} else {
renderWith(
data={errors: product.allErrors()},
status=422
);
}
}
function update() {
if (product.update(params.product)) {
renderWith(product);
} else {
renderWith(
data={errors: product.allErrors()},
status=422
);
}
}
function delete() {
if (product.delete()) {
renderWith(data={message: "Product deleted successfully"});
} else {
renderWith(
data={error: "Could not delete product"},
status=400
);
}
}
private function findProduct() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
renderWith(
data={error: "Product not found"},
status=404
);
}
}
}
wheels generate resource review rating:integer comment:text parent=product
Includes association:
component extends="Model" {
function init() {
belongsTo("product");
property(name="rating", sql="rating");
property(name="comment", sql="comment");
property(name="productId", sql="product_id");
validatesPresenceOf(properties="rating,comment,productId");
validatesNumericalityOf(property="rating", greaterThanOrEqualTo=1, lessThanOrEqualTo=5);
}
}
<cfset resources("products")>
<cfset resources("reviews")>
</cfset>
string
VARCHAR(255)
Length validation
text
TEXT
None by default
integer
INT
Numerical validation
float
DECIMAL
Numerical validation
decimal
DECIMAL
Numerical validation
boolean
BOOLEAN
Boolean validation
date
DATE
Date format validation
datetime
DATETIME
DateTime validation
time
TIME
Time validation
binary
BLOB
None
wheels generate resource user \
email:string:required:unique \
age:integer:min=18:max=120 \
bio:text:limit=1000 \
isActive:boolean:default=true
# Generate only model and migration
wheels generate resource product name:string --skip-controller --skip-views --skip-route
# Generate only controller and views
wheels generate resource product --skip-model --skip-migration
wheels generate resource admin/product name:string namespace=admin
Creates:
/controllers/admin/Products.cfc
/views/admin/products/
Namespaced routes
wheels generate resource product name:string template=custom
Model Test (/tests/models/ProductTest.cfc
):
component extends="wheels.Test" {
function setup() {
super.setup();
model("Product").deleteAll();
}
function test_valid_product_saves() {
product = model("Product").new(
name="Test Product",
price=19.99,
description="Test description"
);
assert(product.save());
assert(product.id > 0);
}
function test_requires_name() {
product = model("Product").new(price=19.99);
assert(!product.save());
assert(ArrayLen(product.errorsOn("name")) > 0);
}
function test_requires_positive_price() {
product = model("Product").new(name="Test", price=-10);
assert(!product.save());
assert(ArrayLen(product.errorsOn("price")) > 0);
}
}
Controller Test (/tests/controllers/ProductsTest.cfc
):
component extends="wheels.Test" {
function test_index_returns_products() {
products = createProducts(3);
result = processRequest(route="products", method="GET");
assert(result.status == 200);
assert(Find("Products", result.body));
assert(FindNoCase(products[1].name, result.body));
}
function test_create_valid_product() {
params = {
product: {
name: "New Product",
price: 29.99,
description: "New product description"
}
};
result = processRequest(route="products", method="POST", params=params);
assert(result.status == 302);
assert(model("Product").count() == 1);
}
}
Use singular names: product
not products
Define all properties: Include types and validations
Add indexes: For frequently queried fields
Include tests: Don't skip test generation
Use namespaces: For admin or API resources
Follow conventions: Stick to RESTful patterns
wheels generate resource product name:string deletedAt:datetime:nullable
wheels generate resource post title:string content:text publishedAt:datetime:nullable status:string:default=draft
wheels generate resource task title:string userId:integer:belongsTo=user completed:boolean:default=false
wheels generate resource category name:string parentId:integer:nullable:belongsTo=category
Create in ~/.wheels/templates/resources/
:
custom-resource/
├── model.cfc
├── controller.cfc
├── views/
│ ├── index.cfm
│ ├── show.cfm
│ ├── new.cfm
│ ├── edit.cfm
│ └── _form.cfm
└── migration.cfc
Available in templates:
${resourceName}
- Singular name
${resourceNamePlural}
- Plural name
${modelName}
- Model class name
${controllerName}
- Controller class name
${tableName}
- Database table name
${properties}
- Array of property definitions
wheels scaffold - Interactive CRUD generation
wheels generate model - Generate models only
wheels generate controller - Generate controllers only
wheels generate api-resource - Generate API resources
wheels generate route - Generate routes only
Generate a complete API resource with model, API controller, and routes.
⚠️ Note: This command is currently marked as broken/disabled in the codebase. The documentation below represents the intended functionality when the command is restored.
The wheels generate api-resource
command creates a complete RESTful API resource including model, API-specific controller (no views), routes, and optionally database migrations and tests. It's optimized for building JSON APIs following REST conventions.
This command is temporarily disabled. Use alternative approaches:
Would generate:
Model: /models/Product.cfc
Controller: /controllers/api/v1/Products.cfc
Route: API namespace with versioning
Migration: Database migration file
Tests: API integration tests
/controllers/api/v1/Products.cfc
:
Generated in /config/routes.cfm
:
Would generate OpenAPI/Swagger documentation:
Until the command is fixed, implement API resources manually:
Create /controllers/api/v1/Products.cfc
manually with the code above.
Version your API: Use URL versioning (/api/v1/)
Use consistent formats: JSON API or custom format
Include pagination: Limit response sizes
Add filtering: Allow query parameters
Implement sorting: Support field sorting
Handle errors consistently: Standard error format
Document thoroughly: OpenAPI/Swagger specs
Add authentication: Secure endpoints
Rate limit: Prevent abuse
Test extensively: Integration tests
- Generate full resources
- Generate controllers
- Generate models
- Generate routes
wheels generate api-resource [name] [properties] [options]
wheels g api-resource [name] [properties] [options]
# Option 1: Use regular resource with --api flag
wheels generate resource product name:string price:float --api
# Option 2: Generate components separately
wheels generate model product name:string price:float
wheels generate controller api/products --api
wheels generate route products --api --namespace=api
name
Resource name (typically singular)
Required
properties
Property definitions (name:type)
--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
--skip-model
Skip model generation
false
--skip-migration
Skip migration generation
false
--skip-tests
Skip test generation
false
--namespace
API namespace
api
--force
Overwrite existing files
false
--help
Show help information
wheels generate api-resource product name:string price:float description:text
component extends="Controller" {
function init() {
provides("json");
// Filters
filters(through="authenticate", except="index,show");
filters(through="findProduct", only="show,update,delete");
filters(through="parseApiParams", only="index");
}
function index() {
// Pagination
page = params.page ?: 1;
perPage = Min(params.perPage ?: 25, 100);
// Filtering
where = [];
if (StructKeyExists(params, "filter")) {
if (StructKeyExists(params.filter, "name")) {
ArrayAppend(where, "name LIKE :name");
params.name = "%#params.filter.name#%";
}
if (StructKeyExists(params.filter, "minPrice")) {
ArrayAppend(where, "price >= :minPrice");
params.minPrice = params.filter.minPrice;
}
}
// Sorting
order = "createdAt DESC";
if (StructKeyExists(params, "sort")) {
order = parseSort(params.sort);
}
// Query
products = model("Product").findAll(
where=ArrayToList(where, " AND "),
order=order,
page=page,
perPage=perPage,
returnAs="objects"
);
// Response
renderWith({
data: serializeProducts(products),
meta: {
pagination: {
page: products.currentPage,
perPage: products.perPage,
total: products.totalRecords,
pages: products.totalPages
}
},
links: {
self: urlFor(route="apiV1Products", params=params),
first: urlFor(route="apiV1Products", params=params, page=1),
last: urlFor(route="apiV1Products", params=params, page=products.totalPages),
prev: products.currentPage > 1 ? urlFor(route="apiV1Products", params=params, page=products.currentPage-1) : "",
next: products.currentPage < products.totalPages ? urlFor(route="apiV1Products", params=params, page=products.currentPage+1) : ""
}
});
}
function show() {
renderWith({
data: serializeProduct(product),
links: {
self: urlFor(route="apiV1Product", key=product.id)
}
});
}
function create() {
product = model("Product").new(deserializeProduct(params));
if (product.save()) {
renderWith(
data={
data: serializeProduct(product),
links: {
self: urlFor(route="apiV1Product", key=product.id)
}
},
status=201,
headers={"Location": urlFor(route="apiV1Product", key=product.id)}
);
} else {
renderWith(
data={
errors: formatErrors(product.allErrors())
},
status=422
);
}
}
function update() {
if (product.update(deserializeProduct(params))) {
renderWith({
data: serializeProduct(product),
links: {
self: urlFor(route="apiV1Product", key=product.id)
}
});
} else {
renderWith(
data={
errors: formatErrors(product.allErrors())
},
status=422
);
}
}
function delete() {
if (product.delete()) {
renderWith(data={}, status=204);
} else {
renderWith(
data={
errors: [{
status: "400",
title: "Bad Request",
detail: "Could not delete product"
}]
},
status=400
);
}
}
// Private methods
private function findProduct() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
renderWith(
data={
errors: [{
status: "404",
title: "Not Found",
detail: "Product not found"
}]
},
status=404
);
}
}
private function authenticate() {
if (!StructKeyExists(headers, "Authorization")) {
renderWith(
data={
errors: [{
status: "401",
title: "Unauthorized",
detail: "Missing authentication"
}]
},
status=401
);
}
// Implement authentication logic
}
private function parseApiParams() {
// Parse JSON API params
if (StructKeyExists(params, "_json")) {
StructAppend(params, params._json, true);
}
}
private function parseSort(required string sort) {
local.allowedFields = ["name", "price", "createdAt"];
local.parts = ListToArray(arguments.sort);
local.order = [];
for (local.part in local.parts) {
local.desc = Left(local.part, 1) == "-";
local.field = local.desc ? Right(local.part, Len(local.part)-1) : local.part;
if (ArrayFindNoCase(local.allowedFields, local.field)) {
ArrayAppend(local.order, local.field & (local.desc ? " DESC" : " ASC"));
}
}
return ArrayToList(local.order);
}
private function serializeProducts(required array products) {
local.result = [];
for (local.product in arguments.products) {
ArrayAppend(local.result, serializeProduct(local.product));
}
return local.result;
}
private function serializeProduct(required any product) {
return {
type: "products",
id: arguments.product.id,
attributes: {
name: arguments.product.name,
price: arguments.product.price,
description: arguments.product.description,
createdAt: DateTimeFormat(arguments.product.createdAt, "iso"),
updatedAt: DateTimeFormat(arguments.product.updatedAt, "iso")
},
links: {
self: urlFor(route="apiV1Product", key=arguments.product.id)
}
};
}
private function deserializeProduct(required struct data) {
if (StructKeyExists(arguments.data, "data")) {
return arguments.data.data.attributes;
}
return arguments.data;
}
private function formatErrors(required array errors) {
local.result = [];
for (local.error in arguments.errors) {
ArrayAppend(local.result, {
status: "422",
source: {pointer: "/data/attributes/#local.error.property#"},
title: "Validation Error",
detail: local.error.message
});
}
return local.result;
}
}
<cfset namespace("api")>
<cfset namespace("v1")>
<cfset resources(name="products", except="new,edit")>
<!--- Additional API routes --->
<cfset post(pattern="products/[key]/activate", to="products##activate", on="member")>
<cfset get(pattern="products/search", to="products##search", on="collection")>
</cfset>
</cfset>
openapi: 3.0.0
info:
title: Products API
version: 1.0.0
paths:
/api/v1/products:
get:
summary: List products
parameters:
- name: page
in: query
schema:
type: integer
- name: perPage
in: query
schema:
type: integer
- name: filter[name]
in: query
schema:
type: string
- name: sort
in: query
schema:
type: string
responses:
200:
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/ProductList'
post:
summary: Create product
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ProductInput'
responses:
201:
description: Created
422:
description: Validation error
wheels generate model product name:string price:float description:text
<!--- In /config/routes.cfm --->
<cfset namespace("api")>
<cfset namespace("v1")>
<cfset resources(name="products", except="new,edit")>
</cfset>
</cfset>
wheels generate test controller api/v1/products
// Bearer token authentication
private function authenticate() {
local.token = GetHttpRequestData().headers["Authorization"] ?: "";
local.token = ReReplace(local.token, "^Bearer\s+", "");
if (!Len(local.token) || !isValidToken(local.token)) {
renderWith(
data={error: "Unauthorized"},
status=401
);
}
}
// In controller init()
filters(through="rateLimit");
private function rateLimit() {
local.key = "api_rate_limit_" & request.remoteAddress;
local.limit = 100; // requests per hour
if (!StructKeyExists(application, local.key)) {
application[local.key] = {
count: 0,
reset: DateAdd("h", 1, Now())
};
}
if (application[local.key].reset < Now()) {
application[local.key] = {
count: 0,
reset: DateAdd("h", 1, Now())
};
}
application[local.key].count++;
if (application[local.key].count > local.limit) {
renderWith(
data={error: "Rate limit exceeded"},
status=429,
headers={
"X-RateLimit-Limit": local.limit,
"X-RateLimit-Remaining": 0,
"X-RateLimit-Reset": DateTimeFormat(application[local.key].reset, "iso")
}
);
}
}
// In controller init()
filters(through="setCorsHeaders");
private function setCorsHeaders() {
header name="Access-Control-Allow-Origin" value="*";
header name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS";
header name="Access-Control-Allow-Headers" value="Content-Type, Authorization";
if (request.method == "OPTIONS") {
renderWith(data={}, status=200);
}
}
component extends="wheels.Test" {
function setup() {
super.setup();
model("Product").deleteAll();
}
function test_get_products_returns_json() {
products = createProducts(3);
result = $request(
route="apiV1Products",
method="GET",
headers={"Accept": "application/json"}
);
assert(result.status == 200);
data = DeserializeJSON(result.body);
assert(ArrayLen(data.data) == 3);
assert(data.meta.pagination.total == 3);
}
function test_create_product_with_valid_data() {
params = {
data: {
type: "products",
attributes: {
name: "Test Product",
price: 29.99,
description: "Test description"
}
}
};
result = $request(
route="apiV1Products",
method="POST",
params=params,
headers={
"Content-Type": "application/json",
"Accept": "application/json"
}
);
assert(result.status == 201);
assert(StructKeyExists(result.headers, "Location"));
data = DeserializeJSON(result.body);
assert(data.data.attributes.name == "Test Product");
}
function test_authentication_required() {
result = $request(
route="apiV1Products",
method="POST",
params={},
headers={"Accept": "application/json"}
);
assert(result.status == 401);
}
}
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
--customize
Interactive customization
false
--force
Overwrite existing files
false
--help
Show help information
wheels generate snippets --list
Output:
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 constraints
wheels generate snippets login-form
Generates:
<!--- 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-filter
Generates:
// 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-delete
Generates:
// 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 --customize
Interactive customization:
? Include user tracking? (Y/n) › Y
? Track IP address? (y/N) › Y
? Track changes in JSON? (Y/n) › Y
Generates:
// 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("model_name", null=false);
t.integer("record_id", null=false);
t.string("action", null=false);
t.text("changes");
t.integer("user_id");
t.string("ip_address");
t.string("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-actions
Generates complete CRUD controller with error handling, pagination, and filters.
wheels generate snippets api-controller
Generates:
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-errors
wheels generate snippets ajax-form
Generates:
<!--- 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-indexes
Generates 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-data
wheels generate snippets --create=my-pattern
Creates template in ~/.wheels/snippets/my-pattern/
:
my-pattern/
├── snippet.json
├── files/
│ ├── controller.cfc
│ ├── model.cfc
│ └── view.cfm
└── README.md
snippet.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=clipboard
wheels generate snippets api-controller --output=file --path=./controllers/Api.cfc
wheels generate snippets --customize
Review 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
Generate frontend code including JavaScript, CSS, and interactive components.
⚠️ Note: This command is currently marked as disabled in the codebase. The documentation below represents the intended functionality when the command is restored.
wheels generate frontend [type] [name] [options]
wheels g frontend [type] [name] [options]
The wheels generate frontend
command creates frontend assets including JavaScript modules, CSS stylesheets, and interactive components. It supports various frontend frameworks and patterns while integrating seamlessly with Wheels views.
This command is temporarily disabled. Use manual approaches:
# Create frontend files manually in:
# /public/javascripts/
# /public/stylesheets/
# /views/components/
type
Type of frontend asset (component, module, style)
Required
name
Name of the asset
Required
--framework
Frontend framework (vanilla, alpine, vue, htmx)
vanilla
--style
CSS framework (none, bootstrap, tailwind)
none
--bundler
Use bundler (webpack, vite, none)
none
--typescript
Generate TypeScript files
false
--test
Generate test files
true
--force
Overwrite existing files
false
--help
Show help information
wheels generate frontend component productCard --framework=alpine
Would generate:
/public/javascripts/components/productCard.js
:
// Product Card Component
document.addEventListener('alpine:init', () => {
Alpine.data('productCard', (initialData = {}) => ({
// State
product: initialData.product || {},
isLoading: false,
isFavorite: false,
// Computed
get formattedPrice() {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(this.product.price || 0);
},
// Methods
async toggleFavorite() {
this.isLoading = true;
try {
const response = await fetch(`/api/products/${this.product.id}/favorite`, {
method: this.isFavorite ? 'DELETE' : 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.getCsrfToken()
}
});
if (response.ok) {
this.isFavorite = !this.isFavorite;
this.$dispatch('favorite-changed', {
productId: this.product.id,
isFavorite: this.isFavorite
});
}
} catch (error) {
console.error('Failed to toggle favorite:', error);
this.$dispatch('notification', {
type: 'error',
message: 'Failed to update favorite status'
});
} finally {
this.isLoading = false;
}
},
async addToCart() {
this.isLoading = true;
try {
const response = await fetch('/api/cart/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.getCsrfToken()
},
body: JSON.stringify({
productId: this.product.id,
quantity: 1
})
});
if (response.ok) {
this.$dispatch('cart-updated');
this.$dispatch('notification', {
type: 'success',
message: 'Added to cart'
});
}
} catch (error) {
console.error('Failed to add to cart:', error);
} finally {
this.isLoading = false;
}
},
getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content || '';
}
}));
});
/public/stylesheets/components/productCard.css
:
/* Product Card Styles */
.product-card {
@apply bg-white rounded-lg shadow-md overflow-hidden transition-transform hover:scale-105;
}
.product-card__image {
@apply w-full h-48 object-cover;
}
.product-card__content {
@apply p-4;
}
.product-card__title {
@apply text-lg font-semibold text-gray-800 mb-2;
}
.product-card__price {
@apply text-xl font-bold text-blue-600 mb-3;
}
.product-card__actions {
@apply flex justify-between items-center;
}
.product-card__button {
@apply px-4 py-2 rounded font-medium transition-colors;
}
.product-card__button--primary {
@apply bg-blue-500 text-white hover:bg-blue-600;
}
.product-card__button--secondary {
@apply bg-gray-200 text-gray-700 hover:bg-gray-300;
}
.product-card__button:disabled {
@apply opacity-50 cursor-not-allowed;
}
/views/components/_productCard.cfm
:
<div class="product-card"
x-data="productCard({
product: #SerializeJSON({
id: arguments.product.id,
name: arguments.product.name,
price: arguments.product.price,
image: arguments.product.imageUrl
})#
})">
<img :src="product.image"
:alt="product.name"
class="product-card__image">
<div class="product-card__content">
<h3 class="product-card__title" x-text="product.name"></h3>
<p class="product-card__price" x-text="formattedPrice"></p>
<div class="product-card__actions">
<button @click="addToCart"
:disabled="isLoading"
class="product-card__button product-card__button--primary">
<span x-show="!isLoading">Add to Cart</span>
<span x-show="isLoading">Adding...</span>
</button>
<button @click="toggleFavorite"
:disabled="isLoading"
class="product-card__button product-card__button--secondary">
<span x-show="!isFavorite">♡</span>
<span x-show="isFavorite">♥</span>
</button>
</div>
</div>
</div>
wheels generate frontend module api --typescript
Would generate /public/javascripts/modules/api.ts
:
// API Module
interface RequestOptions extends RequestInit {
params?: Record<string, any>;
}
interface ApiResponse<T = any> {
data: T;
meta?: Record<string, any>;
errors?: Array<{
field: string;
message: string;
}>;
}
class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseUrl: string = '/api') {
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
};
// Add CSRF token if available
const csrfToken = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
if (csrfToken) {
this.defaultHeaders['X-CSRF-Token'] = csrfToken;
}
}
async request<T = any>(
endpoint: string,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const { params, ...fetchOptions } = options;
// Build URL with params
const url = new URL(this.baseUrl + endpoint, window.location.origin);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
// Merge headers
const headers = {
...this.defaultHeaders,
...fetchOptions.headers
};
try {
const response = await fetch(url.toString(), {
...fetchOptions,
headers
});
if (!response.ok) {
throw new ApiError(response.status, await response.text());
}
const data = await response.json();
return data;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(0, 'Network error');
}
}
get<T = any>(endpoint: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'GET', params });
}
post<T = any>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put<T = any>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
delete<T = any>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
// Export singleton instance
export const api = new ApiClient();
// Export types
export type { ApiResponse, RequestOptions };
export { ApiClient, ApiError };
wheels generate frontend component searchForm --framework=htmx
Would generate:
/views/components/_searchForm.cfm
:
<form hx-get="/products/search"
hx-trigger="submit, keyup changed delay:500ms from:input[name='q']"
hx-target="##search-results"
hx-indicator="##search-spinner"
class="search-form">
<div class="search-form__input-group">
<input type="search"
name="q"
value="#params.q ?: ''#"
placeholder="Search products..."
class="search-form__input">
<button type="submit" class="search-form__button">
Search
</button>
</div>
<div class="search-form__filters">
<select name="category"
hx-get="/products/search"
hx-trigger="change"
hx-target="##search-results"
hx-include="[name='q']"
class="search-form__select">
<option value="">All Categories</option>
<cfloop query="categories">
<option value="#categories.id#"
<cfif params.category == categories.id>selected</cfif>>
#categories.name#
</option>
</cfloop>
</select>
<select name="sort"
hx-get="/products/search"
hx-trigger="change"
hx-target="##search-results"
hx-include="[name='q'],[name='category']"
class="search-form__select">
<option value="relevance">Relevance</option>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="name">Name</option>
</select>
</div>
</form>
<div id="search-spinner" class="htmx-indicator">
<div class="spinner"></div>
</div>
<div id="search-results">
<!--- Results will be loaded here --->
</div>
wheels generate frontend component todoList --framework=vue
Would generate /public/javascripts/components/TodoList.vue
:
<template>
<div class="todo-list">
<h2>{{ title }}</h2>
<form @submit.prevent="addTodo" class="todo-form">
<input
v-model="newTodo"
type="text"
placeholder="Add a new todo..."
class="todo-form__input"
>
<button type="submit" class="todo-form__button">
Add
</button>
</form>
<ul class="todo-items">
<li
v-for="todo in filteredTodos"
:key="todo.id"
class="todo-item"
:class="{ 'todo-item--completed': todo.completed }"
>
<input
type="checkbox"
v-model="todo.completed"
@change="updateTodo(todo)"
>
<span class="todo-item__text">{{ todo.text }}</span>
<button
@click="deleteTodo(todo.id)"
class="todo-item__delete"
>
×
</button>
</li>
</ul>
<div class="todo-filters">
<button
v-for="filter in filters"
:key="filter"
@click="currentFilter = filter"
:class="{ active: currentFilter === filter }"
class="todo-filter"
>
{{ filter }}
</button>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import { api } from '../modules/api';
export default {
name: 'TodoList',
props: {
title: {
type: String,
default: 'Todo List'
}
},
setup() {
const todos = ref([]);
const newTodo = ref('');
const currentFilter = ref('all');
const filters = ['all', 'active', 'completed'];
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed);
case 'completed':
return todos.value.filter(todo => todo.completed);
default:
return todos.value;
}
});
const loadTodos = async () => {
try {
const response = await api.get('/todos');
todos.value = response.data;
} catch (error) {
console.error('Failed to load todos:', error);
}
};
const addTodo = async () => {
if (!newTodo.value.trim()) return;
try {
const response = await api.post('/todos', {
text: newTodo.value,
completed: false
});
todos.value.push(response.data);
newTodo.value = '';
} catch (error) {
console.error('Failed to add todo:', error);
}
};
const updateTodo = async (todo) => {
try {
await api.put(`/todos/${todo.id}`, {
completed: todo.completed
});
} catch (error) {
console.error('Failed to update todo:', error);
todo.completed = !todo.completed;
}
};
const deleteTodo = async (id) => {
try {
await api.delete(`/todos/${id}`);
todos.value = todos.value.filter(todo => todo.id !== id);
} catch (error) {
console.error('Failed to delete todo:', error);
}
};
onMounted(loadTodos);
return {
todos,
newTodo,
currentFilter,
filters,
filteredTodos,
addTodo,
updateTodo,
deleteTodo
};
}
};
</script>
<style scoped>
.todo-list {
max-width: 500px;
margin: 0 auto;
}
.todo-form {
display: flex;
margin-bottom: 1rem;
}
.todo-form__input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
}
.todo-form__button {
padding: 0.5rem 1rem;
background: #007bff;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.todo-items {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
.todo-item--completed .todo-item__text {
text-decoration: line-through;
opacity: 0.6;
}
.todo-item__delete {
margin-left: auto;
background: none;
border: none;
color: #dc3545;
font-size: 1.5rem;
cursor: pointer;
}
.todo-filters {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.todo-filter {
padding: 0.25rem 0.75rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.todo-filter.active {
background: #007bff;
color: white;
border-color: #007bff;
}
</style>
Until the command is fixed, create frontend assets manually:
/public/
├── javascripts/
│ ├── app.js
│ ├── components/
│ ├── modules/
│ └── vendor/
├── stylesheets/
│ ├── app.css
│ ├── components/
│ └── vendor/
└── images/
/public/javascripts/app.js
:
// Main application JavaScript
(function() {
'use strict';
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', function() {
initializeComponents();
setupEventListeners();
loadDynamicContent();
});
function initializeComponents() {
// Initialize all components
document.querySelectorAll('[data-component]').forEach(element => {
const componentName = element.dataset.component;
if (window.components && window.components[componentName]) {
new window.components[componentName](element);
}
});
}
function setupEventListeners() {
// Global event delegation
document.addEventListener('click', handleClick);
document.addEventListener('submit', handleSubmit);
}
function handleClick(event) {
// Handle data-action clicks
const action = event.target.closest('[data-action]');
if (action) {
event.preventDefault();
const actionName = action.dataset.action;
// Handle action
}
}
function handleSubmit(event) {
// Handle AJAX forms
const form = event.target;
if (form.dataset.remote === 'true') {
event.preventDefault();
submitFormAjax(form);
}
}
function submitFormAjax(form) {
const formData = new FormData(form);
fetch(form.action, {
method: form.method,
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
// Handle response
})
.catch(error => {
console.error('Form submission error:', error);
});
}
function loadDynamicContent() {
// Load content marked for lazy loading
const lazyElements = document.querySelectorAll('[data-lazy-load]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadContent(entry.target);
observer.unobserve(entry.target);
}
});
});
lazyElements.forEach(el => observer.observe(el));
}
function loadContent(element) {
const url = element.dataset.lazyLoad;
fetch(url)
.then(response => response.text())
.then(html => {
element.innerHTML = html;
initializeComponents();
});
}
})();
/public/stylesheets/app.css
:
/* Base styles */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #17a2b8;
--light-color: #f8f9fa;
--dark-color: #343a40;
}
/* Layout */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
/* Components */
@import 'components/buttons.css';
@import 'components/forms.css';
@import 'components/cards.css';
@import 'components/modals.css';
/* Utilities */
.hidden {
display: none !important;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
Organize by feature: Group related files together
Use modules: Keep code modular and reusable
Follow conventions: Consistent naming and structure
Progressive enhancement: Work without JavaScript
Optimize performance: Minimize and bundle assets
Test components: Unit and integration tests
Document APIs: Clear component documentation
Handle errors: Graceful error handling
Accessibility: ARIA labels and keyboard support
Security: Validate inputs, use CSRF tokens
wheels generate view - Generate view files
wheels generate controller - Generate controllers
wheels scaffold - Generate complete CRUD
Generate test files for models, controllers, views, and other components.
wheels generate test [type] [name] [options]
wheels g test [type] [name] [options]
The wheels generate test
command creates test files for various components of your Wheels application. It generates appropriate test scaffolding based on the component type and includes common test cases to get you started.
type
Type of test (model, controller, view, helper, route)
Required
name
Name of the component to test
Required
--methods
Specific methods to test
All methods
--integration
Generate integration tests
false
--coverage
Include coverage setup
false
--fixtures
Generate test fixtures
true
--force
Overwrite existing files
false
--help
Show help information
wheels generate test model product
Generates /tests/models/ProductTest.cfc
:
component extends="wheels.Test" {
function setup() {
super.setup();
// Clear test data
model("Product").deleteAll();
// Setup test fixtures
variables.validProduct = {
name: "Test Product",
price: 19.99,
description: "Test product description"
};
}
function teardown() {
super.teardown();
// Clean up after tests
model("Product").deleteAll();
}
// Validation Tests
function test_valid_product_saves_successfully() {
// Arrange
product = model("Product").new(variables.validProduct);
// Act
result = product.save();
// Assert
assert(result, "Product should save successfully");
assert(product.id > 0, "Product should have an ID after saving");
}
function test_product_requires_name() {
// Arrange
product = model("Product").new(variables.validProduct);
product.name = "";
// Act
result = product.save();
// Assert
assert(!result, "Product should not save without name");
assert(ArrayLen(product.errorsOn("name")) > 0, "Should have error on name");
}
function test_product_requires_positive_price() {
// Arrange
product = model("Product").new(variables.validProduct);
product.price = -10;
// Act
result = product.save();
// Assert
assert(!result, "Product should not save with negative price");
assert(ArrayLen(product.errorsOn("price")) > 0, "Should have error on price");
}
function test_product_name_must_be_unique() {
// Arrange
product1 = model("Product").create(variables.validProduct);
product2 = model("Product").new(variables.validProduct);
// Act
result = product2.save();
// Assert
assert(!result, "Should not save duplicate product name");
assert(ArrayLen(product2.errorsOn("name")) > 0, "Should have uniqueness error");
}
// Association Tests
function test_product_has_many_reviews() {
// Arrange
product = model("Product").create(variables.validProduct);
review = product.createReview(rating=5, comment="Great product!");
// Act
reviews = product.reviews();
// Assert
assert(reviews.recordCount == 1, "Product should have one review");
assert(reviews.rating == 5, "Review rating should be 5");
}
// Callback Tests
function test_before_save_sanitizes_input() {
// Arrange
product = model("Product").new(variables.validProduct);
product.name = " Test Product ";
// Act
product.save();
// Assert
assert(product.name == "Test Product", "Name should be trimmed");
}
// Scope Tests
function test_active_scope_returns_only_active_products() {
// Arrange
activeProduct = model("Product").create(
variables.validProduct & {isActive: true}
);
inactiveProduct = model("Product").create(
name="Inactive Product",
price=29.99,
isActive=false
);
// Act
activeProducts = model("Product").active();
// Assert
assert(activeProducts.recordCount == 1, "Should have one active product");
assert(activeProducts.id == activeProduct.id, "Should return active product");
}
// Method Tests
function test_calculate_discount_price() {
// Arrange
product = model("Product").create(variables.validProduct);
// Act
discountPrice = product.calculateDiscountPrice(0.20); // 20% discount
// Assert
expected = product.price * 0.80;
assert(discountPrice == expected, "Discount price should be 80% of original");
}
// Integration Tests
function test_product_lifecycle() {
transaction {
// Create
product = model("Product").new(variables.validProduct);
assert(product.save(), "Should create product");
productId = product.id;
// Read
foundProduct = model("Product").findByKey(productId);
assert(IsObject(foundProduct), "Should find product");
assert(foundProduct.name == variables.validProduct.name, "Should have correct name");
// Update
foundProduct.price = 24.99;
assert(foundProduct.save(), "Should update product");
// Verify update
updatedProduct = model("Product").findByKey(productId);
assert(updatedProduct.price == 24.99, "Price should be updated");
// Delete
assert(updatedProduct.delete(), "Should delete product");
// Verify deletion
deletedProduct = model("Product").findByKey(productId);
assert(!IsObject(deletedProduct), "Product should not exist");
// Rollback transaction
transaction action="rollback";
}
}
}
wheels generate test controller products
Generates /tests/controllers/ProductsTest.cfc
:
wheels generate test view products --name=index
Generates a test for the products/index view.
wheels generate test controller products --crud
Generates complete CRUD test methods for the controller.
component extends="wheels.Test" {
function setup() {
super.setup();
// Setup test data
model("Product").deleteAll();
variables.testProducts = [];
for (i = 1; i <= 3; i++) {
ArrayAppend(variables.testProducts,
model("Product").create(
name="Product #i#",
price=19.99 * i,
description="Description #i#"
)
);
}
}
function teardown() {
super.teardown();
model("Product").deleteAll();
}
// Action Tests
function test_index_returns_all_products() {
// Act
result = processRequest(route="products", method="GET");
// Assert
assert(result.status == 200, "Should return 200 status");
assert(Find("<h1>Products</h1>", result.body), "Should have products heading");
for (product in variables.testProducts) {
assert(Find(product.name, result.body), "Should display product: #product.name#");
}
}
function test_show_displays_product_details() {
// Arrange
product = variables.testProducts[1];
// Act
result = processRequest(route="product", key=product.id, method="GET");
// Assert
assert(result.status == 200, "Should return 200 status");
assert(Find(product.name, result.body), "Should display product name");
assert(Find(DollarFormat(product.price), result.body), "Should display formatted price");
}
function test_show_returns_404_for_invalid_product() {
// Act
result = processRequest(route="product", key=99999, method="GET");
// Assert
assert(result.status == 302, "Should redirect");
assert(result.flash.error == "Product not found.", "Should have error message");
}
function test_new_displays_form() {
// Act
result = processRequest(route="newProduct", method="GET");
// Assert
assert(result.status == 200, "Should return 200 status");
assert(Find("<form", result.body), "Should have form");
assert(Find('name="product[name]"', result.body), "Should have name field");
assert(Find('name="product[price]"', result.body), "Should have price field");
}
function test_create_with_valid_data() {
// Arrange
params = {
product: {
name: "New Test Product",
price: 39.99,
description: "New product description"
}
};
// Act
result = processRequest(route="products", method="POST", params=params);
// Assert
assert(result.status == 302, "Should redirect after creation");
assert(result.flash.success == "Product was created successfully.", "Should have success message");
// Verify product was created
newProduct = model("Product").findOne(where="name='New Test Product'");
assert(IsObject(newProduct), "Product should be created");
assert(newProduct.price == 39.99, "Should have correct price");
}
function test_create_with_invalid_data() {
// Arrange
params = {
product: {
name: "",
price: -10,
description: "Invalid product"
}
};
// Act
result = processRequest(route="products", method="POST", params=params);
// Assert
assert(result.status == 200, "Should render form again");
assert(Find("error", result.body), "Should display errors");
assert(model("Product").count(where="description='Invalid product'") == 0,
"Should not create invalid product");
}
function test_edit_displays_form_with_product_data() {
// Arrange
product = variables.testProducts[1];
// Act
result = processRequest(route="editProduct", key=product.id, method="GET");
// Assert
assert(result.status == 200, "Should return 200 status");
assert(Find('value="#product.name#"', result.body), "Should pre-fill name");
assert(Find(ToString(product.price), result.body), "Should pre-fill price");
}
function test_update_with_valid_data() {
// Arrange
product = variables.testProducts[1];
params = {
product: {
name: "Updated Product Name",
price: 49.99
}
};
// Act
result = processRequest(route="product", key=product.id, method="PUT", params=params);
// Assert
assert(result.status == 302, "Should redirect after update");
assert(result.flash.success == "Product was updated successfully.", "Should have success message");
// Verify update
updatedProduct = model("Product").findByKey(product.id);
assert(updatedProduct.name == "Updated Product Name", "Name should be updated");
assert(updatedProduct.price == 49.99, "Price should be updated");
}
function test_delete_removes_product() {
// Arrange
product = variables.testProducts[1];
initialCount = model("Product").count();
// Act
result = processRequest(route="product", key=product.id, method="DELETE");
// Assert
assert(result.status == 302, "Should redirect after deletion");
assert(result.flash.success == "Product was deleted successfully.", "Should have success message");
assert(model("Product").count() == initialCount - 1, "Should have one less product");
assert(!IsObject(model("Product").findByKey(product.id)), "Product should be deleted");
}
// Filter Tests
function test_authentication_required_for_protected_actions() {
// Test that certain actions require authentication
protectedRoutes = [
{route: "newProduct", method: "GET"},
{route: "products", method: "POST"},
{route: "editProduct", key: variables.testProducts[1].id, method: "GET"},
{route: "product", key: variables.testProducts[1].id, method: "PUT"},
{route: "product", key: variables.testProducts[1].id, method: "DELETE"}
];
for (route in protectedRoutes) {
// Act without authentication
result = processRequest(argumentCollection=route);
// Assert
assert(result.status == 302, "Should redirect unauthenticated user");
assert(result.redirectUrl contains "login", "Should redirect to login");
}
}
// Helper method for processing requests
private function processRequest(
required string route,
string method = "GET",
struct params = {},
numeric key = 0
) {
local.args = {
route: arguments.route,
method: arguments.method,
params: arguments.params
};
if (arguments.key > 0) {
local.args.key = arguments.key;
}
return $processRequest(argumentCollection=local.args);
}
}
wheels generate test view products/index
Generates /tests/views/products/IndexTest.cfc
:
component extends="wheels.Test" {
function setup() {
super.setup();
// Create test data
variables.products = QueryNew(
"id,name,price,createdAt",
"integer,varchar,decimal,timestamp"
);
for (i = 1; i <= 3; i++) {
QueryAddRow(variables.products, {
id: i,
name: "Product #i#",
price: 19.99 * i,
createdAt: Now()
});
}
}
function test_index_view_renders_product_list() {
// Act
result = $renderView(
view="/products/index",
products=variables.products,
layout=false
);
// Assert
assert(Find("<h1>Products</h1>", result), "Should have products heading");
assert(Find("<table", result), "Should have products table");
assert(Find("Product 1", result), "Should display first product");
assert(Find("Product 2", result), "Should display second product");
assert(Find("Product 3", result), "Should display third product");
}
function test_index_view_shows_empty_state() {
// Arrange
emptyQuery = QueryNew("id,name,price,createdAt");
// Act
result = $renderView(
view="/products/index",
products=emptyQuery,
layout=false
);
// Assert
assert(Find("No products found", result), "Should show empty state message");
assert(Find("Create one now", result), "Should have create link");
assert(!Find("<table", result), "Should not show table when empty");
}
function test_index_view_formats_prices_correctly() {
// Act
result = $renderView(
view="/products/index",
products=variables.products,
layout=false
);
// Assert
assert(Find("$19.99", result), "Should format first price");
assert(Find("$39.98", result), "Should format second price");
assert(Find("$59.97", result), "Should format third price");
}
function test_index_view_includes_action_links() {
// Act
result = $renderView(
view="/products/index",
products=variables.products,
layout=false
);
// Assert
assert(Find("New Product", result), "Should have new product link");
assert(FindNoCase("href=""/products/new""", result), "New link should be correct");
// Check action links for each product
for (row in variables.products) {
assert(Find("View</a>", result), "Should have view link");
assert(Find("Edit</a>", result), "Should have edit link");
assert(Find("Delete</a>", result), "Should have delete link");
}
}
function test_index_view_with_pagination() {
// Arrange
paginatedProducts = Duplicate(variables.products);
paginatedProducts.currentPage = 2;
paginatedProducts.totalPages = 5;
paginatedProducts.totalRecords = 50;
// Act
result = $renderView(
view="/products/index",
products=paginatedProducts,
layout=false
);
// Assert
assert(Find("class=""pagination""", result), "Should have pagination");
assert(Find("Previous", result), "Should have previous link");
assert(Find("Next", result), "Should have next link");
assert(Find("Page 2 of 5", result), "Should show current page");
}
function test_index_view_escapes_html() {
// Arrange
productsWithHtml = QueryNew("id,name,price,createdAt");
QueryAddRow(productsWithHtml, {
id: 1,
name: "<script>alert('XSS')</script>",
price: 19.99,
createdAt: Now()
});
// Act
result = $renderView(
view="/products/index",
products=productsWithHtml,
layout=false
);
// Assert
assert(!Find("<script>alert('XSS')</script>", result),
"Should not have unescaped script tag");
assert(Find("<script>", result), "Should have escaped HTML");
}
}
wheels generate test controller products --integration
Generates additional integration tests:
component extends="wheels.Test" {
function test_complete_product_workflow() {
transaction {
// 1. View product list (empty)
result = $visit(route="products");
assert(result.status == 200);
assert(Find("No products found", result.body));
// 2. Navigate to new product form
result = $click("Create one now");
assert(result.status == 200);
assert(Find("<form", result.body));
// 3. Submit new product form
result = $submitForm({
"product[name]": "Integration Test Product",
"product[price]": "29.99",
"product[description]": "Test description"
});
assert(result.status == 302);
assert(result.flash.success);
// 4. View created product
product = model("Product").findOne(order="id DESC");
result = $visit(route="product", key=product.id);
assert(result.status == 200);
assert(Find("Integration Test Product", result.body));
// 5. Edit product
result = $click("Edit");
assert(Find('value="Integration Test Product"', result.body));
result = $submitForm({
"product[name]": "Updated Product",
"product[price]": "39.99"
});
assert(result.status == 302);
// 6. Verify update
result = $visit(route="product", key=product.id);
assert(Find("Updated Product", result.body));
assert(Find("$39.99", result.body));
// 7. Delete product
result = $click("Delete", confirm=true);
assert(result.status == 302);
assert(result.flash.success contains "deleted");
// 8. Verify deletion
assert(!IsObject(model("Product").findByKey(product.id)));
transaction action="rollback";
}
}
}
Focus on:
Validations
Associations
Callbacks
Scopes
Custom methods
Data integrity
Focus on:
Action responses
Parameter handling
Authentication/authorization
Flash messages
Redirects
Error handling
Focus on:
Content rendering
Data display
HTML structure
Escaping/security
Conditional display
Helpers usage
wheels generate test helper format
component extends="wheels.Test" {
function test_format_currency() {
assert(formatCurrency(19.99) == "$19.99");
assert(formatCurrency(1000) == "$1,000.00");
assert(formatCurrency(0) == "$0.00");
assert(formatCurrency(-50.5) == "-$50.50");
}
}
wheels generate test route products
component extends="wheels.Test" {
function test_products_routes() {
// Test route resolution
assert($resolveRoute("/products") == {controller: "products", action: "index"});
assert($resolveRoute("/products/new") == {controller: "products", action: "new"});
assert($resolveRoute("/products/123") == {controller: "products", action: "show", key: "123"});
// Test route generation
assert(urlFor(route="products") == "/products");
assert(urlFor(route="product", key=123) == "/products/123");
assert(urlFor(route="newProduct") == "/products/new");
}
}
wheels generate test model product --fixtures
Creates /tests/fixtures/products.cfc
:
component {
function load() {
// Clear existing data
model("Product").deleteAll();
// Load fixture data
fixtures = [
{
name: "Widget",
price: 19.99,
description: "Standard widget",
categoryId: 1,
isActive: true
},
{
name: "Gadget",
price: 29.99,
description: "Premium gadget",
categoryId: 2,
isActive: true
},
{
name: "Doohickey",
price: 9.99,
description: "Budget doohickey",
categoryId: 1,
isActive: false
}
];
for (fixture in fixtures) {
model("Product").create(fixture);
}
return fixtures;
}
function loadWithAssociations() {
products = load();
// Add reviews
model("Review").create(
productId: products[1].id,
rating: 5,
comment: "Excellent product!"
);
return products;
}
}
// In test file
function assertProductValid(required any product) {
assert(IsObject(arguments.product), "Product should be an object");
assert(arguments.product.id > 0, "Product should have valid ID");
assert(Len(arguments.product.name), "Product should have name");
assert(arguments.product.price > 0, "Product should have positive price");
}
function assertHasError(required any model, required string property) {
local.errors = arguments.model.errorsOn(arguments.property);
assert(ArrayLen(local.errors) > 0,
"Expected error on #arguments.property# but found none");
}
function createTestProduct(struct overrides = {}) {
local.defaults = {
name: "Test Product #CreateUUID()#",
price: RandRange(10, 100) + (RandRange(0, 99) / 100),
description: "Test description",
isActive: true
};
StructAppend(local.defaults, arguments.overrides, true);
return model("Product").create(local.defaults);
}
function createTestUser(struct overrides = {}) {
local.defaults = {
email: "test-#CreateUUID()#@example.com",
password: "password123",
firstName: "Test",
lastName: "User"
};
StructAppend(local.defaults, arguments.overrides, true);
return model("User").create(local.defaults);
}
wheels test
wheels test app tests/models/ProductTest.cfc
wheels test app tests/models/ProductTest.cfc::test_product_requires_name
wheels test --coverage
Test in isolation: Each test should be independent
Use descriptive names: Test names should explain what they test
Follow AAA pattern: Arrange, Act, Assert
Clean up data: Use setup/teardown or transactions
Test edge cases: Empty data, nulls, extremes
Mock external services: Don't rely on external APIs
Keep tests fast: Optimize slow tests
Test one thing: Each test should verify one behavior
Use fixtures wisely: Share common test data
Run tests frequently: Before commits and in CI
function test_private_method_through_public_interface() {
// Don't test private methods directly
// Test them through public methods that use them
product = model("Product").new(name: " Test ");
product.save(); // Calls private sanitize method
assert(product.name == "Test");
}
function test_expiration_date() {
// Use specific dates instead of Now()
testDate = CreateDate(2024, 1, 1);
product = model("Product").new(
expiresAt: DateAdd("d", 30, testDate)
);
// Test with mocked current date
request.currentDate = testDate;
assert(!product.isExpired());
request.currentDate = DateAdd("d", 31, testDate);
assert(product.isExpired());
}
function test_random_discount() {
// Test the range, not specific values
product = model("Product").new(price: 100);
for (i = 1; i <= 100; i++) {
discount = product.getRandomDiscount();
assert(discount >= 0.05 && discount <= 0.25,
"Discount should be between 5% and 25%");
}
}
wheels test run - Run tests
wheels test coverage - Test coverage
Testing Guide - Testing documentation
Complete reference for all Wheels CLI commands organized by category.
Essential commands for managing your Wheels application.
Enhanced server commands that wrap CommandBox's native functionality with Wheels-specific features.
Validates Wheels application directory
Shows framework-specific information
Integrates with application reload
Provides helpful error messages
Commands for generating application code and resources.
Common options across generators:
--force
- Overwrite existing files
--help
- Show command help
Commands for managing database schema and migrations.
Commands for running and managing tests.
--watch
- Auto-run on changes
--reporter
- Output format (simple, json, junit)
--bundles
- Specific test bundles
--labels
- Filter by labels
Commands for managing application configuration.
Commands for managing development environments and application context.
Commands for managing Wheels plugins.
--global
- Install/list globally
--dev
- Development dependency
Commands for analyzing code quality and patterns.
Commands for security scanning and hardening.
--fix
- Auto-fix issues
--path
- Specific path to scan
Commands for optimizing application performance.
Commands for generating and serving documentation.
--format
- Output format (html, markdown)
--output
- Output directory
--port
- Server port
Commands for managing application maintenance mode and cleanup tasks.
--force
- Skip confirmation prompts
--dryRun
- Preview changes without executing
Commands for continuous integration and deployment workflows.
Commands for Docker container management and deployment.
Commands for managing application deployments.
--environment
- Target environment
--force
- Force deployment
--dry-run
- Preview changes without deploying
Every command supports --help
:
Many commands have shorter aliases:
Creating a new feature:
Starting development:
Deployment preparation:
Interactive debugging:
Running maintenance scripts:
Some commands in the Wheels CLI are currently in various states of development or maintenance:
wheels docs
- Base documentation command is currently broken
wheels generate api-resource
- API resource generation is currently broken
The following commands exist in the codebase but are currently disabled:
Some CI and Docker commands have disabled variants in the codebase
These commands may be re-enabled in future versions of Wheels.
wheels generate app [name]
Create new application
wheels scaffold [name]
Generate complete CRUD
wheels dbmigrate latest
Run database migrations
wheels test run
Run application tests
wheels server start
Start development server
wheels server status
Check server status
wheels watch
Watch files for changes
wheels reload
Reload application
wheels init
Bootstrap existing app for CLI
wheels info
Display version information
wheels reload [mode]
Reload application
wheels deps
Manage dependencies
wheels destroy [type] [name]
Remove generated code
wheels watch
Watch for file changes
wheels server
Display server commands help
wheels server start
Start development server
wheels server stop
Stop development server
wheels server restart
Restart server and reload app
wheels server status
Show server status with Wheels info
wheels server log
Tail server logs
wheels server open
Open application in browser
wheels generate app
wheels new
Create new application
wheels generate app-wizard
Interactive app creation
wheels generate controller
wheels g controller
Generate controller
wheels generate model
wheels g model
Generate model
wheels generate view
wheels g view
Generate view
wheels generate property
Add model property
wheels generate route
Generate route
wheels generate resource
REST resource
wheels generate api-resource
API resource (Currently broken)
wheels generate frontend
Frontend code
wheels generate test
Generate tests
wheels generate snippets
Code snippets
wheels scaffold
Complete CRUD
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
wheels db schema
Export/import schema
wheels db seed
Seed database
wheels test [type]
Run framework tests
wheels test run [spec]
Run TestBox tests
wheels test coverage
Generate coverage report
wheels test debug
Debug test execution
wheels config list
List configuration
wheels config set [key] [value]
Set configuration
wheels config env
Environment config
wheels environment
Display/switch environment
wheels environment set [env]
Set environment with reload
wheels environment list
List available environments
wheels console
Interactive REPL console
wheels runner [script]
Execute scripts with context
wheels env
Environment base command
wheels env setup [name]
Setup environment
wheels env list
List environments
wheels env switch [name]
Switch environment
wheels plugins
Plugin management base command
wheels plugins list
List plugins
wheels plugins install [name]
Install plugin
wheels plugins remove [name]
Remove plugin
wheels analyze
Code analysis base command
wheels analyze code
Analyze code quality
wheels analyze performance
Performance analysis
wheels analyze security
Security analysis (deprecated)
wheels security
Security management base command
wheels security scan
Scan for vulnerabilities
wheels optimize
Optimization base command
wheels optimize performance
Optimize application
wheels docs
Documentation base command (Currently broken)
wheels docs generate
Generate documentation
wheels docs serve
Serve documentation
wheels maintenance:on
Enable maintenance mode
wheels maintenance:off
Disable maintenance mode
wheels cleanup:logs
Remove old log files
wheels cleanup:tmp
Remove temporary files
wheels cleanup:sessions
Remove expired sessions
wheels ci init
Initialize CI/CD configuration
wheels docker init
Initialize Docker configuration
wheels docker deploy
Deploy using Docker
wheels deploy
Deployment base command
wheels deploy audit
Audit deployment configuration
wheels deploy exec
Execute deployment
wheels deploy hooks
Manage deployment hooks
wheels deploy init
Initialize deployment
wheels deploy lock
Lock deployment state
wheels deploy logs
View deployment logs
wheels deploy proxy
Configure deployment proxy
wheels deploy push
Push deployment
wheels deploy rollback
Rollback deployment
wheels deploy secrets
Manage deployment secrets
wheels deploy setup
Setup deployment environment
wheels deploy status
Check deployment status
wheels deploy stop
Stop deployment
wheels [command] --help
wheels generate controller --help
wheels dbmigrate create table --help
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 myapp
wheels scaffold name=product properties=name:string,price:decimal
wheels dbmigrate latest
wheels test run
wheels server start # Start the server
wheels watch # Terminal 1: Watch for file changes
wheels server log # Terminal 2: Monitor logs
wheels test run --watch # Terminal 3: Run tests in watch mode
wheels test run
wheels security scan
wheels optimize performance
wheels dbmigrate info
wheels environment production
wheels console # Start REPL
wheels console environment=testing # Test in specific env
wheels console execute="model('User').count()" # Quick check
wheels runner scripts/cleanup.cfm
wheels runner scripts/migrate.cfm environment=production
wheels runner scripts/report.cfm params='{"month":12}'
WHEELS_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