From b356465ca49d26030a854eabd4fea22659826b09 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 9 Jan 2026 17:48:12 +0500 Subject: [PATCH 001/405] cli/guides adjustments analyze commands --- cli/src/commands/wheels/analyze/code.cfc | 4 +- cli/src/commands/wheels/analyze/security.cfc | 31 ---------------- docs/src/SUMMARY.md | 1 - .../commands/analysis/analyze-security.md | 37 ------------------- .../commands/analysis/analyze.md | 12 +----- 5 files changed, 4 insertions(+), 81 deletions(-) delete mode 100644 cli/src/commands/wheels/analyze/security.cfc delete mode 100644 docs/src/command-line-tools/commands/analysis/analyze-security.md diff --git a/cli/src/commands/wheels/analyze/code.cfc b/cli/src/commands/wheels/analyze/code.cfc index 4500f16ba1..a3842fa6c5 100644 --- a/cli/src/commands/wheels/analyze/code.cfc +++ b/cli/src/commands/wheels/analyze/code.cfc @@ -190,8 +190,8 @@ component extends="../base" { var icon = getSeverityIcon(issue.severity); var color = getSeverityColor(issue.severity); - detailOutput.colored(" #icon# Line #issue.line#:#issue.column# - #issue.message#", color); - print.grayLine(" Rule: #issue.rule#" & (issue.fixable ? " [Auto-fixable]" : "")).toConsole(); + detailOutput.output(" #icon# Line #issue.line#:#issue.column# - #issue.message#"); + print.cyanLine(" Rule: #issue.rule#" & (issue.fixable ? " [Auto-fixable]" : "")).toConsole(); } } } diff --git a/cli/src/commands/wheels/analyze/security.cfc b/cli/src/commands/wheels/analyze/security.cfc deleted file mode 100644 index 537e9db0fc..0000000000 --- a/cli/src/commands/wheels/analyze/security.cfc +++ /dev/null @@ -1,31 +0,0 @@ -/** - * DEPRECATED: Use 'wheels security scan' instead - * This command is maintained for backward compatibility only - */ -component extends="../base" { - - property name="detailOutput" inject="DetailOutputService@wheels-cli"; - - /** - * @deprecated Use 'wheels security scan' instead - */ - function run( - string path = ".", - boolean fix = false, - string report = "console", - string severity = "medium", - boolean deep = false - ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs(arguments); - print.yellowLine("DEPRECATED: This command is deprecated").toConsole(); - print.yellowLine("Please use 'wheels security scan' instead").toConsole(); - detailOutput.line(); - - // Forward to new command - detailOutput.output("Wait Running Command 'wheels security scan'..."); - command("wheels security scan") - .params(argumentCollection = arguments) - .run(); - } -} \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 3fd139cb63..0ab81473a7 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -66,7 +66,6 @@ * Code Analysis * [wheels analyze code](command-line-tools/commands/analysis/analyze-code.md) * [wheels analyze performance](command-line-tools/commands/analysis/analyze-performance.md) - * [wheels analyze security](command-line-tools/commands/analysis/analyze-security.md) * Config * [wheels config check](command-line-tools/commands/config/config-check.md) * [wheels config diff](command-line-tools/commands/config/config-diff.md) diff --git a/docs/src/command-line-tools/commands/analysis/analyze-security.md b/docs/src/command-line-tools/commands/analysis/analyze-security.md deleted file mode 100644 index a7f8621c35..0000000000 --- a/docs/src/command-line-tools/commands/analysis/analyze-security.md +++ /dev/null @@ -1,37 +0,0 @@ -# analyze security (Deprecated) - -**⚠️ DEPRECATED**: This command has been deprecated. Please use `wheels security scan` instead. - -## Migration Notice - -The `analyze security` command has been moved to provide better organization and expanded functionality. - -### Old Command (Still Works) -```bash -wheels analyze security -``` - -### New Command -```bash -wheels security scan [path] [--fix] [--output=] [--detailed] -``` - - -## Why the Change? - -- Better command organization with dedicated security namespace -- Enhanced scanning capabilities -- Improved reporting options -- Integration with security vulnerability databases - -## See Also - -- [security scan](../security/security-scan.md) - The replacement command with enhanced features - -## Deprecation Timeline - -- **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. \ No newline at end of file diff --git a/docs/src/command-line-tools/commands/analysis/analyze.md b/docs/src/command-line-tools/commands/analysis/analyze.md index 9ea4d7d1d5..949bf2bc1b 100644 --- a/docs/src/command-line-tools/commands/analysis/analyze.md +++ b/docs/src/command-line-tools/commands/analysis/analyze.md @@ -11,7 +11,7 @@ wheels analyze [subcommand] [options] ## Description -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. +The `wheels analyze` command provides comprehensive code analysis tools for Wheels applications. It helps identify code quality issues, performance bottlenecks, and provides actionable insights for improvement. ## Subcommands @@ -19,7 +19,6 @@ The `wheels analyze` command provides comprehensive code analysis tools for Whee |---------|-------------| | `code` | Analyze code quality and patterns | | `performance` | Analyze performance characteristics | -| `security` | Security vulnerability analysis (deprecated) | ## Direct Usage @@ -32,7 +31,7 @@ wheels analyze help ## Analysis Overview -The analyze [code, performance, security] commands examines: +The analyze [code, performance] commands examines: ### Code Quality - Coding standards compliance @@ -46,12 +45,6 @@ The analyze [code, performance, security] commands examines: - Memory usage patterns - Cache effectiveness -### Security -- SQL injection risks -- XSS vulnerabilities -- Insecure configurations -- Outdated dependencies - ## Best Practices 1. Run analysis regularly @@ -83,5 +76,4 @@ The analyze [code, performance, security] commands examines: - [wheels analyze code](analyze-code.md) - Code quality analysis - [wheels analyze performance](analyze-performance.md) - Performance analysis -- [wheels security scan](../security/security-scan.md) - Security scanning - [wheels test](../testing/test.md) - Run tests \ No newline at end of file From 8d63a2dba22ffdb074dc53d08d93325916f6218c Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 9 Jan 2026 19:06:04 +0500 Subject: [PATCH 002/405] cli/assets commands adjustments --- cli/src/commands/wheels/analyze/code.cfc | 6 ----- .../commands/wheels/analyze/performance.cfc | 4 +--- cli/src/commands/wheels/assets/precompile.cfc | 22 +++++++++---------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/cli/src/commands/wheels/analyze/code.cfc b/cli/src/commands/wheels/analyze/code.cfc index a3842fa6c5..f702d1b144 100644 --- a/cli/src/commands/wheels/analyze/code.cfc +++ b/cli/src/commands/wheels/analyze/code.cfc @@ -313,12 +313,6 @@ component extends="../base" { arrayAppend(recommendations, "Address code smells to improve maintainability"); } - if (!fileExists(".wheelscheck")) { - arrayAppend(recommendations, "Create a .wheelscheck config file for custom rules"); - } - - arrayAppend(recommendations, "Integrate this check into your CI/CD pipeline"); - for (var rec in recommendations) { detailOutput.output(" * #rec#"); } diff --git a/cli/src/commands/wheels/analyze/performance.cfc b/cli/src/commands/wheels/analyze/performance.cfc index 6d46c82154..e4be90110a 100644 --- a/cli/src/commands/wheels/analyze/performance.cfc +++ b/cli/src/commands/wheels/analyze/performance.cfc @@ -500,9 +500,7 @@ component extends="../base" { var score = calculatePerformanceScore(arguments.results); var grade = getPerformanceGrade(score); - detailOutput.divider("=", 50); - print.boldLine("Performance Grade: #grade# (#score#/100)").toConsole(); - detailOutput.divider("=", 50); + detailOutput.header("Performance Grade: #grade# (#score#/100)"); detailOutput.line(); // Recommendations diff --git a/cli/src/commands/wheels/assets/precompile.cfc b/cli/src/commands/wheels/assets/precompile.cfc index ad38e3e270..4eccc97360 100644 --- a/cli/src/commands/wheels/assets/precompile.cfc +++ b/cli/src/commands/wheels/assets/precompile.cfc @@ -50,7 +50,7 @@ component extends="../base" { // Normalize environment aliases arguments.environment = normalizeEnvironment(arguments.environment); - print.greenBoldLine("==> Precompiling assets for #arguments.environment#...").toConsole(); + detailOutput.output("Precompiling assets for #arguments.environment#..."); detailOutput.line(); // Define asset directories @@ -74,7 +74,7 @@ component extends="../base" { // Process JavaScript files if (directoryExists(jsDir)) { - print.boldLine("Processing JavaScript files...").toConsole(); + detailOutput.output("Processing JavaScript files..."); var jsFiles = directoryList(jsDir, true, "query", "*.js"); for (var file in jsFiles) { if (file.type == "File" && !findNoCase(".min.js", file.name)) { @@ -91,7 +91,7 @@ component extends="../base" { // Process CSS files if (directoryExists(cssDir)) { - print.boldLine("Processing CSS files...").toConsole(); + detailOutput.output("Processing CSS files..."); var cssFiles = directoryList(cssDir, true, "query", "*.css"); for (var file in cssFiles) { if (file.type == "File" && !findNoCase(".min.css", file.name)) { @@ -108,7 +108,7 @@ component extends="../base" { // Process image files if (directoryExists(imagesDir)) { - print.boldLine("Processing image files...").toConsole(); + detailOutput.output("Processing image files..."); var imageFiles = directoryList(imagesDir, true, "query"); for (var file in imageFiles) { if (file.type == "File" && isImageFile(file.name)) { @@ -128,16 +128,16 @@ component extends="../base" { detailOutput.output("Asset manifest written to: #manifestPath#"); detailOutput.line(); - print.greenBoldLine("==> Asset precompilation complete!").toConsole(); - print.greenLine(" Processed #processedCount# files").toConsole(); - detailOutput.output(" Compiled assets location: #compiledDir#"); + detailOutput.statusSuccess("Asset precompilation complete!"); + detailOutput.output("Processed #processedCount# files", true); + detailOutput.output("Compiled assets location: #compiledDir#", true); // Provide instructions for production detailOutput.line(); - print.yellowLine("To use precompiled assets in production:").toConsole(); - detailOutput.output("1. Configure your web server to serve static files from /public/assets/compiled"); - detailOutput.output("2. Update your application to use the asset manifest for cache-busted URLs"); - detailOutput.output("3. Set wheels.assetManifest = true in your production environment"); + detailOutput.output("To use precompiled assets in production:"); + detailOutput.output("1. Configure your web server to serve static files from /public/assets/compiled", true); + detailOutput.output("2. Update your application to use the asset manifest for cache-busted URLs", true); + detailOutput.output("3. Set wheels.assetManifest = true in your production environment", true); } /** From 874c82e70d0f0eb5254670572fdf612d7a70ca1c Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Sun, 11 Jan 2026 18:16:47 -0800 Subject: [PATCH 003/405] Bump version to 3.0.1-SNAPSHOT for development Co-Authored-By: Claude Opus 4.5 --- core/box.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/box.json b/core/box.json index b05b3e58aa..504d63fbb6 100644 --- a/core/box.json +++ b/core/box.json @@ -1,5 +1,5 @@ { "name": "wheels-core", - "version": "3.0.0", + "version": "3.0.1-SNAPSHOT", "location": "https://github.com/cfwheels/cfwheels/tree/3.0/core" } \ No newline at end of file From c8a93e7660582ed76ee3a3671f4444553e0b1070 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Sun, 11 Jan 2026 18:23:46 -0800 Subject: [PATCH 004/405] Update CLI template options for 3.0 stable release - Change default template from 3.0.0-rc to 3.0 stable - Add Bleeding Edge template option - Remove deprecated 2.5.x template option - Update documentation comments Co-Authored-By: Claude Opus 4.5 --- cli/src/commands/wheels/generate/app-wizard.cfc | 4 ++-- cli/src/commands/wheels/generate/app.cfc | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/src/commands/wheels/generate/app-wizard.cfc b/cli/src/commands/wheels/generate/app-wizard.cfc index f72aa3384f..b8da11e2d1 100644 --- a/cli/src/commands/wheels/generate/app-wizard.cfc +++ b/cli/src/commands/wheels/generate/app-wizard.cfc @@ -156,8 +156,8 @@ component aliases="wheels g app-wizard, wheels new" extends="../base" { var template = multiselect( 'Which Wheels Template shall we use? ' ) .options( [ - {value: 'wheels-base-template@^3.0.0-rc.1', display: '3.0.0-rc - Wheels Base Template - Release Candidate', selected: true}, - {value: 'cfwheels-base-template', display: '2.5.x - Wheels Base Template - Stable Release'}, + {value: 'wheels-base-template@^3.0.0', display: '3.0 - Wheels Base Template - Stable', selected: true}, + {value: 'wheels-base-template@BE', display: 'Bleeding Edge - Wheels Base Template'}, {value: 'cfwheels-template-htmx-alpine-simple', display: 'Wheels Template - HTMX - Alpine.js - Simple.css'}, {value: 'wheels-starter-app', display: 'Wheels Starter App'}, {value: 'cfwheels-todomvc-htmx', display: 'Wheels - TodoMVC - HTMX - Demo App'}, diff --git a/cli/src/commands/wheels/generate/app.cfc b/cli/src/commands/wheels/generate/app.cfc index ce8cad1669..7ee92fb2c6 100644 --- a/cli/src/commands/wheels/generate/app.cfc +++ b/cli/src/commands/wheels/generate/app.cfc @@ -14,8 +14,8 @@ * {code} * * Here are the basic templates that are available for you that come from ForgeBox - * - Wheels Base Template - 3.0 Bleeding Edge (default) - * - CFWheels Base Template - 2.5 Stable + * - Wheels Base Template - 3.0 Stable (default) + * - Wheels Base Template - Bleeding Edge * - Wheels Template - HelloWorld * - Wheels Template - HelloDynamic * - Wheels Template - HelloPages @@ -55,7 +55,7 @@ component aliases="wheels g app" extends="../base" { /** * @name The name of the app you want to create - * @template The name of the app template to generate (or an endpoint ID like a forgebox slug). Default is Base@BE (Bleeding Edge) + * @template The name of the app template to generate (or an endpoint ID like a forgebox slug). Default is Bleeding Edge * @directory The directory to create the app in * @reloadPassword The reload passwrod to set for the app * @datasourceName The datasource name to set for the app From 8d702f8f58c24dce0e9945549475715ec6c12a78 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Sun, 11 Jan 2026 19:20:37 -0800 Subject: [PATCH 005/405] Set base template version to 3.0.1-SNAPSHOT for develop branch This ensures snapshot builds from develop produce proper SNAPSHOT versions instead of conflicting with stable release versions. Co-Authored-By: Claude Opus 4.5 --- templates/base/src/box.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base/src/box.json b/templates/base/src/box.json index 30b6cfad96..8ade09612c 100644 --- a/templates/base/src/box.json +++ b/templates/base/src/box.json @@ -1,6 +1,6 @@ { "name":"Wheels.fw", - "version":"3.0.0", + "version":"3.0.1-SNAPSHOT", "author":"Wheels Core Team and Community", "shortDescription":"Wheels MVC Framework Core Directory", "location":"ForgeboxStorage", From c18e2d3c0de09364e56e0e88555ed5b26b220525 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 14 Jan 2026 18:57:57 +0500 Subject: [PATCH 006/405] cli/output fixes --- cli/src/commands/wheels/about.cfc | 2 +- cli/src/commands/wheels/env/setup.cfc | 35 ++++++++++++----------- cli/src/models/EnvironmentService.cfc | 17 +++++------ core/src/wheels/public/layout/_footer.cfm | 4 +++ 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/cli/src/commands/wheels/about.cfc b/cli/src/commands/wheels/about.cfc index 42d2b46c37..e05f38378a 100644 --- a/cli/src/commands/wheels/about.cfc +++ b/cli/src/commands/wheels/about.cfc @@ -99,7 +99,7 @@ component extends="base" { private string function getWheelsCliVersion() { // Read from CLI module's box.json - local.boxJsonPath = getDirectoryFromPath(getCurrentTemplatePath()) & "../../../box.json"; + local.boxJsonPath = expandPath("/wheels-cli/box.json"); if (FileExists(local.boxJsonPath)) { try { local.boxJson = DeserializeJSON(FileRead(local.boxJsonPath)); diff --git a/cli/src/commands/wheels/env/setup.cfc b/cli/src/commands/wheels/env/setup.cfc index 3e3f8419a9..7f5bc03a7d 100644 --- a/cli/src/commands/wheels/env/setup.cfc +++ b/cli/src/commands/wheels/env/setup.cfc @@ -77,9 +77,9 @@ component extends="../base" { detailOutput.statusWarning("Environment '#arguments.environment#' already exists."); detailOutput.statusInfo("What would you like to do?"); detailOutput.line(); - detailOutput.output(" 1. Overwrite entire environment file", true); - detailOutput.output(" 2. Update only database variables (preserve other settings)", true); - detailOutput.output(" 3. Cancel", true); + detailOutput.output("1. Overwrite entire environment file", true); + detailOutput.output("2. Update only database variables (preserve other settings)", true); + detailOutput.output("3. Cancel", true); detailOutput.line(); var choice = ask("Select option [1-3]: "); @@ -87,15 +87,15 @@ component extends="../base" { switch(choice) { case "1": updateMode = "overwrite"; - detailOutput.statusSuccess("Will overwrite environment file..."); + detailOutput.output("Will overwrite environment file..."); break; case "2": updateMode = "update"; - detailOutput.statusSuccess("Will update only database variables..."); + detailOutput.output("Will update only database variables..."); break; case "3": default: - detailOutput.statusWarning("Environment setup cancelled."); + detailOutput.statusFailed("Environment setup cancelled."); return; } detailOutput.line(); @@ -205,21 +205,11 @@ component extends="../base" { detailOutput.statusWarning("Database creation failed - #e.message#"); detailOutput.line(); detailOutput.statusInfo("You can create it manually with:"); - detailOutput.output(" wheels db create datasource=#datasourceName# database=#databaseName# environment=#arguments.environment# dbtype=#arguments.dbtype#", true); + detailOutput.output("wheels db create datasource=#datasourceName# database=#databaseName# environment=#arguments.environment# dbtype=#arguments.dbtype#", true); detailOutput.line(); } } - if (result.keyExists("nextSteps") && arrayLen(result.nextSteps)) { - detailOutput.statusInfo("Next Steps:"); - detailOutput.line(); - - for (var step in result.nextSteps) { - detailOutput.output(" - #step#", true); - } - detailOutput.line(); - } - // Show summary detailOutput.subHeader("Summary"); detailOutput.metric("Environment", arguments.environment); @@ -231,6 +221,17 @@ component extends="../base" { } detailOutput.metric("Debug Mode", arguments.debug ? "Enabled" : "Disabled"); detailOutput.metric("Cache Mode", arguments.cache ? "Enabled" : "Disabled"); + detailOutput.line(); + + if (result.keyExists("nextSteps") && arrayLen(result.nextSteps)) { + detailOutput.statusInfo("Next Steps:"); + detailOutput.line(); + + for (var step in result.nextSteps) { + detailOutput.output("- #step#", true); + } + detailOutput.line(); + } } else { detailOutput.error("Setup failed: #result.error#"); diff --git a/cli/src/models/EnvironmentService.cfc b/cli/src/models/EnvironmentService.cfc index 91735c408d..45a8dc4aa1 100644 --- a/cli/src/models/EnvironmentService.cfc +++ b/cli/src/models/EnvironmentService.cfc @@ -1188,20 +1188,17 @@ box server start port=8080 host=0.0.0.0"; switch (arguments.template) { case "docker": - arrayAppend(steps, "1. Start Docker environment: docker-compose -f docker-compose.#arguments.environment#.yml up"); - arrayAppend(steps, "2. Access application at: http://localhost:8080"); - arrayAppend(steps, "3. Stop environment: docker-compose -f docker-compose.#arguments.environment#.yml down"); + arrayAppend(steps, "Start Docker environment: docker-compose -f docker-compose.#arguments.environment#.yml up"); + arrayAppend(steps, "Stop environment: docker-compose -f docker-compose.#arguments.environment#.yml down"); break; case "vagrant": - arrayAppend(steps, "1. Start Vagrant VM: vagrant up"); - arrayAppend(steps, "2. Access application at: http://localhost:8080 or http://192.168.56.10:8080"); - arrayAppend(steps, "3. SSH into VM: vagrant ssh"); - arrayAppend(steps, "4. Stop VM: vagrant halt"); + arrayAppend(steps, "Start Vagrant VM: vagrant up"); + arrayAppend(steps, "SSH into VM: vagrant ssh"); + arrayAppend(steps, "Stop VM: vagrant halt"); break; default: - arrayAppend(steps, "1. Switch to environment: wheels env switch #arguments.environment#"); - arrayAppend(steps, "2. Start server: box server start"); - arrayAppend(steps, "3. Access application at: http://localhost:8080"); + arrayAppend(steps, "Switch to environment: wheels env switch #arguments.environment#"); + arrayAppend(steps, "Start server: box server start"); } return steps; diff --git a/core/src/wheels/public/layout/_footer.cfm b/core/src/wheels/public/layout/_footer.cfm index fa111751e3..48e597d259 100644 --- a/core/src/wheels/public/layout/_footer.cfm +++ b/core/src/wheels/public/layout/_footer.cfm @@ -122,24 +122,28 @@ $(".functiondefinition").show(); $(".functionlink").show(); updateFunctionCount(); + $('.ui.sticky').sticky('refresh'); } function filterByFunctionName(name){ $("#function-output").find(".functiondefinition").hide().end() .find("[data-function='" + name + "']").show().end() .find("#" + name).show(); window.location.hash="#" + name; + $('.ui.sticky').sticky('refresh'); } function filterByCategory(section, category){ $("#function-navigation").find(".functionlink").hide().end() .find('[data-section="' + section + '"][data-category="' + category + '"]').show(); $("#function-output").find(".functiondefinition").hide().end() .find('[data-section="' + section + '"][data-category="' + category + '"]').show(); + $('.ui.sticky').sticky('refresh'); } function filterBySection(section){ $("#function-navigation").find(".functionlink").hide().end() .find('[data-section="' + section + '"]').show(); $("#function-output").find(".functiondefinition").hide().end() .find('[data-section="' + section + '"]').show(); + $('.ui.sticky').sticky('refresh'); } function updateFunctionCount(){ $("#functionResults .resultCount").html($("#function-output .functiondefinition:visible").length); From 01509e1af0d71f87cc9529721019d685c6c9641c Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 15 Jan 2026 12:33:09 +0500 Subject: [PATCH 007/405] commit: update wheels macOS installer fixes minor issues --- .../installer/macos/WheelsInstallerApp.swift | 20 +- tools/installer/macos/install-wheels | 372 ++++++++++++++---- .../macos/installer/wheels-installer.dmg | Bin 367369 -> 376978 bytes 3 files changed, 297 insertions(+), 95 deletions(-) diff --git a/tools/installer/macos/WheelsInstallerApp.swift b/tools/installer/macos/WheelsInstallerApp.swift index e282f4f109..7467a735f2 100644 --- a/tools/installer/macos/WheelsInstallerApp.swift +++ b/tools/installer/macos/WheelsInstallerApp.swift @@ -116,11 +116,11 @@ class WheelsInstallerApp: NSObject, NSApplicationDelegate { templatePopup = NSPopUpButton(frame: NSRect(x: 180, y: yPosition - 3, width: 400, height: 25)) templatePopup.addItems(withTitles: [ - "wheels-base-template@BE (3.0.x Bleeding Edge)", - "wheels-base-template@stable (2.5.x Stable)", - "wheels-htmx-template (HTMX + Alpine.js)", - "wheels-starter-template (Starter App)", - "wheels-todomvc-template (TodoMVC Demo)" + "3.0 - Wheels Base Template - Stable", + "Bleeding Edge - Wheels Base Template", + "Wheels Template - HTMX - Alpine.js - Simple.css", + "Wheels Starter App", + "Wheels - TodoMVC - HTMX - Demo App" ]) contentView.addSubview(templatePopup) yPosition -= 35 @@ -457,12 +457,12 @@ class WheelsInstallerApp: NSObject, NSApplicationDelegate { func getTemplateValue() -> String { let title = templatePopup.titleOfSelectedItem ?? "" + if title.contains("3.0 - Wheels Base Template") { return "wheels-base-template@^3.0.0" } if title.contains("Bleeding Edge") { return "wheels-base-template@BE" } - if title.contains("Stable") { return "wheels-base-template@stable" } - if title.contains("HTMX") { return "wheels-htmx-template" } - if title.contains("Starter") { return "wheels-starter-template" } - if title.contains("TodoMVC") { return "wheels-todomvc-template" } - return "wheels-base-template@BE" + if title.contains("HTMX - Alpine.js - Simple.css") { return "cfwheels-template-htmx-alpine-simple" } + if title.contains("Wheels Starter App") { return "wheels-starter-app" } + if title.contains("TodoMVC - HTMX") { return "cfwheels-todomvc-htmx" } + return "wheels-base-template@^3.0.0" } func getEngineValue() -> String { diff --git a/tools/installer/macos/install-wheels b/tools/installer/macos/install-wheels index 1f47f3e2d9..f73f555f9b 100755 --- a/tools/installer/macos/install-wheels +++ b/tools/installer/macos/install-wheels @@ -23,7 +23,7 @@ readonly COMMANDBOX_DOWNLOAD_URL="https://downloads.ortussolutions.com/ortussolu # ============================= INSTALL_PATH="$HOME/Desktop/commandbox" APP_NAME="MyWheelsApp" -TEMPLATE="wheels-base-template@BE" +TEMPLATE="wheels-base-template@^3.0.0" RELOAD_PASSWORD="changeMe" DATASOURCE_NAME="MyWheelsApp" CFML_ENGINE="lucee" @@ -34,10 +34,18 @@ APP_BASE_PATH="$HOME/Desktop/Sites" FORCE=false SKIP_PATH=false -START_TIME=$(date +%s) -LOG_FILE="${TMPDIR}wheels-installation.log" TEMP_FILES=() +# ============================= +# Temporary installer scripts & status flags +# ============================= +TEMP_DIR="${TMPDIR:-/tmp}/wheels-installer" +mkdir -p "$TEMP_DIR" + +# Unified installation variables +INSTALL_FLAG="$TEMP_DIR/install-status.txt" +UNIFIED_INSTALL_SCRIPT="$TEMP_DIR/install-dependencies.sh" + # ============================= # Parse Command Line Arguments # ============================= @@ -102,6 +110,10 @@ done # Logging Functions # ============================= +START_TIME=$(date +%s) +mkdir -p "${INSTALL_PATH}/logs" +LOG_FILE="${INSTALL_PATH}/logs/wheels-installation.log" + log_info() { echo "[$(date '+%H:%M:%S')] INFO: $1" echo "[$(date '+%H:%M:%S')] INFO: $1" >> "$LOG_FILE" @@ -158,7 +170,7 @@ stop_with_error() { echo " • Report Issues: https://github.com/wheels-dev/wheels/issues" echo " • Discussion: https://github.com/wheels-dev/wheels/discussions" echo "" - echo "Log file saved to: $LOG_FILE" + echo "You can view the installation log here: $LOG_FILE" echo "" cleanup_temp_files exit 1 @@ -168,96 +180,261 @@ stop_with_error() { # Installation Functions # ============================= +check_existing_installations() { + log_section "EXISTING INSTALLATIONS CHECK" + + # Check application directory + local app_path="${APP_BASE_PATH}/${APP_NAME}" + if [[ -d "$app_path" ]] && ! $FORCE; then + stop_with_error "Application directory already exists: $app_path" + fi + + # Check CommandBox servers if CommandBox is installed + local box_path="${INSTALL_PATH}/box" + if [[ -f "$box_path" ]] && "$box_path" version &> /dev/null; then + + # Check for existing servers with the same name (moved logic) + local servers_json + if servers_json=$("$box_path" server list --json 2>/dev/null); then + if [[ -n "$servers_json" ]]; then + local existing_server + existing_server=$(echo "$servers_json" | grep -o '"name":"'"$APP_NAME"'"' || true) + + if [[ -n "$existing_server" ]]; then + local server_details="Name: $APP_NAME" + local weburl + weburl=$(echo "$servers_json" | grep -o '"weburl":"[^"]*"' | head -1 | cut -d'"' -f4 || true) + local webroot + webroot=$(echo "$servers_json" | grep -o '"webroot":"[^"]*"' | head -1 | cut -d'"' -f4 || true) + + if [[ -n "$weburl" ]]; then + server_details+="\nURL: $weburl" + fi + if [[ -n "$webroot" ]]; then + server_details+="\nWebroot: $webroot" + fi + + stop_with_error "A CommandBox server already exists with this name: $APP_NAME" + fi + fi + fi + fi + log_success "No conflicting installations found" +} + check_java() { log_section "JAVA VERSION CHECK" local java_found=false local java_version=0 - local needs_install=false + local homebrew_needed=false + local java_needed=false - # Check if Java is installed + # ------------------------------- + # Detect existing Java + # ------------------------------- if command -v java &> /dev/null; then - # Get Java version - local java_version_output=$(java -version 2>&1 | head -n 1) + local java_version_output + java_version_output=$(java -version 2>&1 | head -n 1) - # Parse version (handles both old format "1.8.0" and new format "17.0.1") if [[ $java_version_output =~ \"([0-9]+)\.([0-9]+) ]]; then local major="${BASH_REMATCH[1]}" local minor="${BASH_REMATCH[2]}" - # Old format: "1.8.0" means Java 8 if [[ $major == "1" ]]; then java_version=$minor else - # New format: "17.0.1" means Java 17 java_version=$major fi + java_found=true log_info "Found Java version: $java_version" fi fi - # Check if Java meets requirements + # ------------------------------- + # Check what needs to be installed + # ------------------------------- if [[ $java_found == false ]] || [[ $java_version -lt $MINIMUM_JAVA_VERSION ]]; then - needs_install=true - + java_needed=true if [[ $java_found == true ]]; then log_error "Java version $java_version is below minimum requirement (${MINIMUM_JAVA_VERSION})" else - log_error "Java not found in PATH" + log_error "Java not found" fi + fi - echo "" - echo "CommandBox requires Java ${MINIMUM_JAVA_VERSION} or higher." - echo "" + if [[ $java_needed == true ]]; then + log_info "CommandBox requires Java ${MINIMUM_JAVA_VERSION} or higher." - # Check if Homebrew is available for automatic installation - if command -v brew &> /dev/null; then - log_info "Homebrew detected - installing Java ${MINIMUM_JAVA_VERSION} automatically..." - echo "" + # Ensure brew path is included for checking + if [ -d "/opt/homebrew/bin" ]; then + export PATH="/opt/homebrew/bin:$PATH" + elif [ -d "/usr/local/bin" ]; then + export PATH="/usr/local/bin:$PATH" + fi - if brew install openjdk@${MINIMUM_JAVA_VERSION} 2>&1; then - # Add to PATH for current session - if [[ -d "/opt/homebrew/opt/openjdk@${MINIMUM_JAVA_VERSION}/bin" ]]; then - export PATH="/opt/homebrew/opt/openjdk@${MINIMUM_JAVA_VERSION}/bin:$PATH" - elif [[ -d "/usr/local/opt/openjdk@${MINIMUM_JAVA_VERSION}/bin" ]]; then - export PATH="/usr/local/opt/openjdk@${MINIMUM_JAVA_VERSION}/bin:$PATH" - fi + if ! command -v brew &> /dev/null; then + homebrew_needed=true + log_error "Homebrew not found" + else + log_success "Homebrew detected" + fi - # Verify installation - if command -v java &> /dev/null; then - local new_version_output=$(java -version 2>&1 | head -n 1) - log_success "Java ${MINIMUM_JAVA_VERSION} installed successfully" - log_info "Version: $new_version_output" - echo "" - return 0 + # ------------------------------- + # Install dependencies via single terminal session + # ------------------------------- + install_dependencies_via_terminal "$homebrew_needed" "$java_needed" + + # Wait for installation completion + local counter=0 + while true; do + counter=$((counter+1)) + if [[ -f "$INSTALL_FLAG" ]]; then + local result + result=$(cat "$INSTALL_FLAG") + if [[ "$result" == "success" ]]; then + log_success "Dependencies installed successfully" + rm -f "$UNIFIED_INSTALL_SCRIPT" "$INSTALL_FLAG" + # Update PATH after successful installation + if [ -d "/opt/homebrew/bin" ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [ -d "/usr/local/bin" ]; then + eval "$(/usr/local/bin/brew shellenv)" + fi + export PATH="$(brew --prefix openjdk@${MINIMUM_JAVA_VERSION})/bin:$PATH" + break else - log_error "Java installation completed but 'java' command not found in PATH" - stop_with_error "Please restart your terminal and run the installer again" + stop_with_error "Dependency installation failed" fi else - log_error "Failed to install Java via Homebrew" - stop_with_error "Please install Java manually and try again" + log_info "Waiting for dependency installation… (${counter} checks so far)" fi - else - # No Homebrew - stop with instructions - log_error "Homebrew not found - cannot auto-install Java" - echo "" - echo "Please install Java ${MINIMUM_JAVA_VERSION} or higher manually:" - echo "" - echo "Option 1 - Install Homebrew first (recommended):" - echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" - echo " brew install openjdk@${MINIMUM_JAVA_VERSION}" - echo "" - echo "Option 2 - Download Java directly:" - echo " - Temurin (recommended): https://adoptium.net/temurin/releases/" - echo " - Oracle: https://www.oracle.com/java/technologies/downloads/" - echo "" - stop_with_error "Java ${MINIMUM_JAVA_VERSION}+ is required" - fi + sleep 2 + done + else + log_success "Java version meets requirements (>= ${MINIMUM_JAVA_VERSION})" fi +} + +install_dependencies_via_terminal() { + local need_homebrew=$1 + local need_java=$2 + + log_section "DEPENDENCY INSTALLATION" + + rm -f "$INSTALL_FLAG" + + log_info "Creating unified dependency installer script" - log_success "Java version meets requirements (>= ${MINIMUM_JAVA_VERSION})" + cat > "$UNIFIED_INSTALL_SCRIPT" <> "$UNIFIED_INSTALL_SCRIPT" <<'EOF' +echo "Step 1: Installing Homebrew..." +echo "You may be asked for your macOS password." +echo "" + +# Run official Homebrew installer +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +# Add Homebrew to PATH +if [ -d "/opt/homebrew/bin" ]; then + echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +if [ -d "/usr/local/bin" ]; then + echo 'eval "$(/usr/local/bin/brew shellenv)"' >> ~/.zprofile + eval "$(/usr/local/bin/brew shellenv)" +fi + +# Check if brew installed successfully +if ! command -v brew >/dev/null 2>&1; then + echo "failed" > "$STATUS_FILE" + echo "" + echo "Homebrew installation failed." + echo "Status file created at: $STATUS_FILE" + exit 1 +fi + +echo "Homebrew installed successfully." +echo "" + +EOF + fi + + # Add Java installation if needed + if [[ "$need_java" == true ]]; then + cat >> "$UNIFIED_INSTALL_SCRIPT" <> "\$SHELL_PROFILE" +fi + +# Verify Java +if command -v java >/dev/null 2>&1; then + echo "" + echo "Java installed successfully." + echo "Version: \$(java -version 2>&1 | head -n 1)" +else + echo "failed" > "\$STATUS_FILE" + echo "" + echo "Java installation failed." + echo "Status file created at: \$STATUS_FILE" + exit 1 +fi + +EOF + fi + + # Add completion message to script + cat >> "$UNIFIED_INSTALL_SCRIPT" <<'EOF' +echo "" +echo "All dependencies installed successfully!" +echo "success" > "$STATUS_FILE" +echo "" +echo "You can now close this Terminal window." +EOF + + chmod +x "$UNIFIED_INSTALL_SCRIPT" + log_info "Launching Terminal for dependency installation" + open -a Terminal "$UNIFIED_INSTALL_SCRIPT" } install_commandbox() { @@ -302,9 +479,26 @@ add_to_path() { return 0 fi - local shell_rc="$HOME/.zshrc" - [[ "$SHELL" == */bash ]] && shell_rc="$HOME/.bash_profile" + # Detect shell and choose appropriate rc file + local shell_rc + case "$SHELL" in + */zsh) + shell_rc="$HOME/.zshrc" + [[ ! -f "$shell_rc" ]] && shell_rc="$HOME/.zprofile" ;; + */bash) + shell_rc="$HOME/.bash_profile" + [[ ! -f "$shell_rc" ]] && shell_rc="$HOME/.bashrc" ;; + */fish) + shell_rc="$HOME/.config/fish/config.fish" ;; + *) + shell_rc="$HOME/.profile" ;; + esac + + # Ensure file exists + mkdir -p "$(dirname "$shell_rc")" + touch "$shell_rc" + # Check if path already exists if grep -q "export PATH=\"$INSTALL_PATH:\$PATH\"" "$shell_rc" 2>/dev/null; then log_success "Already in PATH" return 0 @@ -337,6 +531,39 @@ create_application() { local box_path="${INSTALL_PATH}/box" local app_path="${APP_BASE_PATH}/${APP_NAME}" + log_info "Checking if server already exists for: $APP_NAME" + + # Check for existing servers with the same name + local servers_json + if servers_json=$("$box_path" server list --json 2>/dev/null); then + + if [[ -n "$servers_json" ]]; then + # Parse JSON and check for existing server with same name + local existing_server + existing_server=$(echo "$servers_json" | grep -o '"name":"'"$APP_NAME"'"' || true) + + if [[ -n "$existing_server" ]]; then + # Extract server details + local server_details="Name: $APP_NAME" + local weburl + weburl=$(echo "$servers_json" | grep -o '"weburl":"[^"]*"' | head -1 | cut -d'"' -f4 || true) + local webroot + webroot=$(echo "$servers_json" | grep -o '"webroot":"[^"]*"' | head -1 | cut -d'"' -f4 || true) + + if [[ -n "$weburl" ]]; then + server_details+="\nURL: $weburl" + fi + if [[ -n "$webroot" ]]; then + server_details+="\nWebroot: $webroot" + fi + + stop_with_error "A CommandBox server already exists with this name." + fi + fi + else + stop_with_error "Failed to check existing servers Application Name: $APP_NAME" + fi + log_info "Creating Wheels application: $APP_NAME" log_info "Location: $app_path" log_info "Template: $TEMPLATE" @@ -348,11 +575,6 @@ create_application() { echo " Please be patient..." echo "" - # Check existing - if [[ -d "$app_path" ]]; then - stop_with_error "Application directory already exists: $app_path" - fi - mkdir -p "$APP_BASE_PATH" cd "$APP_BASE_PATH" @@ -395,27 +617,6 @@ create_application() { fi } -start_server() { - log_section "DEVELOPMENT SERVER" - - local box_path="${INSTALL_PATH}/box" - local app_path="${APP_BASE_PATH}/${APP_NAME}" - - cd "$app_path" - - log_info "Starting server..." - "$box_path" server start 2>&1 - - sleep 3 - - local server_url=$("$box_path" server status --json 2>/dev/null | grep -o '"defaultBaseURL":"[^"]*"' | cut -d'"' -f4 || echo "") - - if [[ -n "$server_url" ]]; then - log_success "Server running at: $server_url" - echo "$server_url" > "${TMPDIR}wheels-server-url.txt" - fi -} - show_completion() { local duration=$(($(date +%s) - START_TIME)) local box_path="${INSTALL_PATH}/box" @@ -470,6 +671,7 @@ main() { echo "" # Perform installation + check_existing_installations check_java install_commandbox add_to_path diff --git a/tools/installer/macos/installer/wheels-installer.dmg b/tools/installer/macos/installer/wheels-installer.dmg index 7db70414a0fe54ae3ebc6b7e3f8b3a1e5c38044f..4e9567a2fab98b6281a6536389c26d400a3875a3 100644 GIT binary patch delta 213539 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�(6 zN2c44o~E7WROKACBk;sFa;IFn&HpB$NZ~xE%2*oG%o9|dkIM{fK6|dJVW0Cmj*ZKv zJlCo&WoVRDOPwOqj3`Apdy~3DGPMmmcbFX}gJsFlGlTYMegZ;1Cu%?}>HS%hVrRW! z9PpsZ95J?3^#R6?6MwpBU(3kFvURe#rmo>w4&%7zzLeiG4nJoA$=ZHdHlrCgWv4@g zzqsgyVmjl+FTuoxR0+#!)QkRtT#^D0KHADxXdenFc2<*wBr|w7ffG4TmKQ^J_{rwU zBZk)a@yq{kXY=az7A&|RVgV)?#T$~9rN36}&T0b+JOKS!)qgkF8So!u<}Tgiz0oR; zo=oq(_{_tDuFS7sZh6a2z8lxzWKtu-3owRWlq&!YV*`_~FidMBKx|Uz zwVu~wF1f=4)qfSvED!OqvA)Obl?%m;eBui@WHC^KdXA=qZpeG+V3YQFY}Q}r`;s@` zZXL-%P|lB2rVNXwk^%1kaP%^{*BV^FQYA!uA$wi&zm6o*hSZ=_BfvlY4_@H2hyyWt7K#)hMr4zB}z+auy&d+EqWM)Iy*N> zT?t<5iTuTLd07xFfIlW29j7(2^mv%kAPk&7 z8oJpbl7CmG?@A0kP0b>D12Vf!Jh#SfZ64pxKC8GG-Bk9>&tk^aXfLLu+yp3E0wLBC z(h5+kE<1G8!oey2H6^=r`<>9JhBoIdGPKJ;#n!O3A!nXRpU#3c+U&qm-DAJjJ&WTR z1C(sfaA%^M=1L<^k26y_ZnIb(y0#()j2W}OXMgzY;I9Qw++*KsZZ+l=DhP+bHz#C_ z4(~@>aDH*Vwt*h&M1g{mXHJ@Dr=yTw=Xt={Q}o^MqPIDH*f;lL z|D%(#UiLeG1ikrS;QPR4+yh}ZTksia?27_Vsy=l)Om8OcnfuwlT>9m3a8pke(qtn& zqkoJmGheF}^Ali7(Px%&+p5j6u_ll67>uKUOvk$K3M~MUjG7P>i1v2)4yD2yM7gnx zyFampF7%#1&cD^#54tD?7w@nOZi=od{kTkA?XFQmtcVB%$F!ba;3yPg3{U@uvE74U zhu2R1*l!ARv{hE`ytWCHj_%$c>LF4Ie2IFtZ?`4lxYOxlo!P`(i9VtaetRv zd86rxU7t%Ztk)InXpipkawmDl?}Y->?0KWiZ>un3uwL1r2+%pUE5$;ZWR+zPM-5#3 zT{`?N;vxU0R*`7+bU%E*cA3J+w2k5Hk}Q`whbE&m9AB+jkhn4x$WHU){EALt#qdyw>{R%*1ee2tgT%DXd{9I*SJ;`KDY=4amlxV|>O3WjZe}6OtzH%VQ z3knVG6WowlX`ZI9I|o{YAU7z>k=6GY74P>Rh<+kA`wo?YfUk{E`H4&c3144MWLw8! zxGU8q%Q2(xt-2{V0RvtI`AY2|rJ(|$t;HEJuz&zPVjC*_T5RfMIaZsHz;pjlD$_u= zy7!sJKGWuZE#A$-G*)y1et+jfh@6W$pJt?L!w>8yBz6y{!Q4-+nPN1Bw#T_eE)V!Y zdnEvB!9*JZW0eD4#_UYvq^etXSXBF+oc_tCPghW^F97H-j)CSmvuzSK)>s?lz}hjZ zMMG@?Q&mOAkuQ&-taMsDQZ=+mr|aizi%!6a#VFi z?tze(V`;VprRVpzOz!|+20Y_MHVYV!SwiV!_jse8U32*(5}_?;z*UDGq))#Edj1SR zevTj)#)?Jg;HQ|MH-7^k-}ni-pmU0o#Lzup1j(_i|Jf4<>^3icW)Zc}-Z#QwqRb~8 z$v^Nt$%7zD9HG)pyEUpjt#Hf;wZ;(5w=J>-qS?rr zcz78neE zA$V7WS${La94vV!#TnK3HgjDkDGdgAI!ox#h|7}4!})UaHGhp0)_!&_6qFIbd(aw& z_VMHGxWV*T9X%cIoqmccrGfiL99`k7zZ&+PhFfm{TZvsD!0SdbW5!PxHy z57Zb2)QBpKLt$-cBm;^$7`Mr$o$efipd7jOB|5VN$!xdM%ld@%2UkR%BLrfh7@CHb z*_4$f(81aKIpY$9ALzNRUtkIVrCgu-Ey)M4?*5yP=Wo0`a#OC_QJ2^vpcdZW5?r5? zFn_2#AQEIW|GYd`MzN8sa!>^a zYl1RSnZsqb4*Yhr8_CJS_6uuRLK<^O?sdW)Bx*hh1_))e1@IbfP(}83H7dcwl3H_Z z&6O=UVS1?3KwOVIDvae;e5)!{qZfwiaDT)el0cY)HOp|{@KH_n6jek7Q{4mZ&(P4j3_}c%#u!0W zW{PuVE2EREWxWa~_3@Y5WC&GsP8#OqHHZ#K2$kG$$X%}NDf{_N7DRJBvjX%L@qe0( z&(m?(W(4#WICEkjIS1U-q<o#o{RsF?i3}^Jcc1%5D@L_~;VVrPg3O1sCz|SM zfM%MiZ9uOF7JHkoAj8&tDBeYt+`}(=2zN4;#~H`c4M* z9-z+(@rqz-@iZU$k>QIw_XbXcWMKAWXJ`P7J9y`uSGU$&?X8>h;=@Jg3@w@IJl8xk z;>#oAJ~1?S&H6ev20ys!iGN1lzc&uHYj7L&^kV}+GKpRL{cL9Uw_b0yD1L;u7a-0T z3(G8eB_R9Fej1$6YZTl&b4t;e!WalAsJM7U1EPlE^Hky-I}Ss!gquU zs5DfOz692E$hxg&<@4f<&}moe@5FcHmPdd#8XRVJX}_Mm6{(|8+yP2eun&596=(9g zW92_dB`{pL1UC&4jDMOds?rTeTH2{2DRpTC-tP~oOgGi7tiC*>Ge4KLw%9f@5Ymt? zD9c85NblHSjR90wzgb$(u|bC3i_>GSWHk_@m@#1{2LQy$dGvEl%|emX32iABHG!*g#W_dt}gJz%RRDT^gqV^pZXU{>z462&H zyVb7tRSkjh3@pnDI&sRz&f#aW9jFHpU(j`g7KCv1Ku*)BfBFGZj@LE=^lT?zyQow0CW6Z9_%o zH58!-olzCIB7a&T_FfA+U_u6xQnNftL2jplL#lgWp>Smv`LK@nXOK^fQj4=b)rR}V zwP^bm4TBA88NhsmC=#ld~`faH%r z^EQoHl?A4-69`5BP>BR3s4Mw(%Sk1e*rX`rn|2LSZsu8?)<|gioGfKHltj7JJG6>JN`KIZ9>|TJ0;im~WKi${*L5m{0#NP#Q} zG3~R<5#UL-V;tK81FL1#4F^_0C(T`<#5{c)#($_Qp)`PX@>k?}!bLVXI|f+0pcSPp zVsiTOOQHq9VF5AX}jF{1(f1EOsqj(sg{u+BdG5* zz<(uiwjukkLu;P84{!;ts+&0B8&VMG85NT$cxVz)g%Mb?$dZ6G_{*>mdX~BF4-ra_ zb*Nbol9`_w_-EX*?g)NA{O6T$%cfdx7O|;a+am#hnD&nTQrJ`#Eyu$$YLp@)VXmh4 z${uWa8z1hq)5M*=$!EoH`5-U3jF6t0q<{Oszy_U%Ax{h~Ql|FT3>;jT`oJfXaBg{u z={2I$7j2>juC>mlG?UUR2&2q-0w2FHyFb}K$u+l*!hrlooQr@vzS$1{u~|QVL=bT)UU9Th!Idsf06@t8F@FOk z*-!VWq$V@e+)il@xi7vL@CK9^Kn^<|iP{S^S$lQA5gpUfc$qCxK!2nKT8kdN+W_O#FXb&bX@-cjL8?Fig(Bg)!j&lGQo!b9w^C%c&+RHR^>6J=2y_cjR6j`9GpkvFi+@~M*dw7| z^#`n`^*r{5JZJ_i$e@>0r`oSVb-V}=*PG7tzx{nDpA7B^_WwRHW7#tMz)6KMUcEsT z48JwUM?a-vD|=QsIuxlO{iJ^|>0XZ2t!IHcop-3oV!mv8r0|Di1jZ)kf?^q>z$)L$9_}%e z1~=+hK?~p7RuW{2gs(M51Dc1{I5t6NK@;eub^%aun3n;o`34L4L7kyD$(tmxq77D4YYM+bY zQ>%?OjOKH~9MIIhs-_;R9cgwP1iDRgSTRzQkpEqWaBt6Y(kh8$H$8<;q<~Vmwr=4~ ztE3hIm^$xAK?KNz=HPmg77wCx`I>12}<7xRYMop*-b>+9f#x>VS|@w6#NM0 z15>D_L7h6V4`mZi6@N8x$XAf|wHnfKZ=+%&iH#I&f2*8pxHSJA`D>m;0R*oZ7K~#n zl?`Y1E>++^MM2PP@N3FK21}jx5nZv_dXD4omCs=zgi2BcOh%;%RwVDw@i0!=%3??e zWY!trKL|4`6E|hqJ(#6sZWO{T0A-j6q*gyt;>|oemg@x@zJFIxcgCTysi3XpK~7gvqKXP}G^XSLYYe_Mw9b z9toy%qP&BeM5`U?15F(P&Og@1VU*;sy$;g! zC_tbMQZ-OaYe*F<|9S)u;%*Nzw`1+Xj*(#9enJU9ytGXOu}D_#{+jJHb)1kkelWy% z`I-^i+rscTQIX1RQ8@cYfTD<=ee8~(L@9f0>}6gG%ztOUaY4M^RYhV01k8E#nM?c= zs^TTP`c0d>HB|0P^cD^ZA<`eMeD3NkTaRyRk4#+5g{|2RMO(=qUP(|uA;8ET=A{}A z9LB9bxwfl&0%A<){Faa40)k7TkqjpCDx+)q4*;`Yg3(q|ec}^xDPku8)aGUWMpRpy zA|!CG34a5uM-CW)8`xPfgc?>2uQsK+aJFuDxv)n=EvMtNLF)j}7?#KxNbB~5Ah@wC z`F0QP)!sOfcsRy2(D7b1D+q+4z_MTVV31OEscBvl2S#woa2#_xwnq@0@5;gXe`esG*N34!eU}rkwyg*FtWY+8i4>vY zvCu#&w%cD9`DX6%2Y*jfD3q!g_6WJPZO#B>l!RsVgzwgIs7?ra;oQ zs(-chE(vJTP;3EKEU$^b_4t-B+j~Yw4W29lXXlm36k=5fvjaL-GjFMneuE+noWErp zBH%yhzmxheclfM(@2y2R%%ZbTp2fAl=CTvZZuQ*dnZH{48&uX?7@Fe&S~r$cC?DUsT~D0MKr(ZN%alLuzy|-4b3Nax5L_74+hAEzzQ-P+DGb^9`E`;gZ{$E zNJ0M$R84D9zxkAgsee^4*9>EPAm0Q@I_<{1Up@aa(}RwvB;CE==GG6%Ya(G(W!90{ zrlr|hBi6~I4Uh%R<5Hk(aAU1YH=p9d!{kVc*b3rm?ax#Bz(QG~Huy~OhJS~}ZhF3z zmL%C+{@ zx%BzuUDUw95ShEP%4tH6zBoKgo7sE2-g^7K@Na&`;YZ%`%=G}Gk$;6kPY2*Lv)RyG zS4Q=c044?U6>pIYW&14bcWOk}cT5(vJKUr7YBri7ld`R>VfaquIV$+SOfsibsf!17 zBjsEjQG@m#6duZ=sz|tSq3I)HlgnJ!(*X|?w{xe#d;eXYqDh&pjq6mO#Ml&(3J&JV#hTIN4abHOu=J`h%Uxg9J0x6|K zCA-q>A0vrbOyA_p>$Kw2SM5?GuP_CXtn)I7^~Ry{J7XWb;}@7j0~a?gl&wmX$Hh|| z1~e43k&OouP|?PnA~S_2K>N(I4oBv|Z+=v>W9#AXVa~p}VSffC1T(>>6=#T!sdK^p zUGIO;ck+dCT_#YCts|R?TC5Di454_*n;2!w>BlmJ%zLrj`=9>ypJJB$otN_MVogK=_E1Wr6gQ4y3!Gxom*W$)r?buKd zM=@#hurFk+1bDY`iQPinTy3q7l7Q`e3BPT5JsG(aE?tJdCV7u0wel1^% zv7JGr2}-5%we8af1x3@+(M;Ez8wm)@z5yIwY1fnq7k>|H_wIkPfUw?1_r;fRM@}(W zkGp&&7I;_jZY5*6S-;;^gXwn-yt8@M4+Odu8r?HdNNDe}7S zG*gH`O_;*p{N`#CUoRqnUBfe)nuckZ09D$cr;U?Odh8}G(%pnjjsU9x!fG&bR=CxW zy0P6E-~_JiE!45iO~Z(_X(zY)8i=yxWhWX%uYb2<=J4BPjyDZ-bsiE40=oql+2dz2 z=;~x+L2H)*NPX2)QW~}4BT_xaKhE9upYm_gZJIa|YS1jqRhhQU@L>t$8YXd=n^|ezG3^~E;;f%@c(0|2*QYs9bM)rD zZzE=`74tEP)6J;{mE!>%So^6N1^rqxmN6X~%;$n^LeA1TQwNW0JU}}78Mf6wC$6u< zJEpf@<{a=X?_chpe&3-aMITl{_T#tvg{y7S8q$poXS)PJPa zB&U!Vd|hoTx~Br-JO<(|@_t-dvqjo(K?@}x(e)|C4;O2_ipZl3qn79VJ z1@WxpG`ae(!~OJ2AXp3;NB%2AA693{`Ec&;{P#!vn>MyFij9Zk@~DhjWCmX324Db_ z&4&LKn0L#(k-=!M5xtX;23Y5fNq;&@>exCF2O-I}jl;_rw)<|7NB(Y^_3Vem{@H-< zOw*zC1k=(~;w$CpsByqJZs`H*2Shk%7VB7*3Q~T>#0%W-zlTix*EpJx#neU6K_BW! z+bsj4!Y!*N>L|vmH7NO-?@!pUx!l~(3DoS|db;By&|~~EL0r}%QjD`VqJLrIF5-fs zI0ivS8L0!nMU{iyhd%~TND#9r3n{;6?4tC1ake2Ho~fIcq^t+UidS2+vOkfa9Zn(I za$SCAZ;)N__O`h`%g

)XI;$++fK<_>582a3R<2UIi1d2342r8$6d=c5zyNn_O$+mE2DuTjcuypUu2{G1S z-eQF&g3#=$08pxemii!*Z(h*#y{_S%AJ`!D2eFHsiGeX1v}=?)$YoOH{Q=Ke>Bp>Z zti$%MIiIlLp@;s@)7O#l^M`+Y|8`D}(wLxw7!I=_lP z4my7$B`x{q`5uO4$GIKkx{oEh9hCGYOdOpK`hvYclEeGip0d3+WVL$4p)5S-?O7?U z9J~>+uA^Z-oZ$ho-6a)ezl2=9eyV`#|BSkEKNEE5{_RmfFo-gnu>4T_*Ww^!Dzm9iyaA77CRdog3!>-*-XRfn9$PYasJKoQrjs`V)j+RGQ>I% zLL_($kXdG2vEBd$AZ3B>w9H;TD8K#6V7Jqmvwq;VMt_*b*X1$iIPuviO6uk~kCz9a91R*(3!Q`h!FT4V=Td$feS;E`oKWLD3^>d9U~4`?~Lc zvA8e0P{afogMy$a6&r(18CZ1GeOIj?xJFP~*6(czKof9m6xT9*Ff*jE-ytM#74Hn) zWouiZbARv`LX)}Z!0EMqfbin-8%14^!#27Qc!b)Zk?we+^#ey3QoZUK;x4d=9E{gu z1f>*?xgvD&JwL2*S18p%Lv5q7uF^%+oFIQjx_Q~@fo8n4<8w?9*9c(;)MQb?9K zDD?oFq0ECpxv?$1;P!3MKRBBQ6+1YwAAcaO1s04-CJ??u1i%=j6<2KTg5UAc10;;s zPuP!99x}?0|H69?c%}WSK*yOE{CV+fuQ^#w7B`oEA5VAbm3`&zE!Ep6ee}b{9p6~V zs&A_{A@uO@Ln`Xk{w^@_`vV$P78BMYvL7``F^(eeaG~NmMJi#0M3gaRQjU+L{C|xJ zNW-ce)??ryr>(!+!`1NN{o?r}zYbz9(OV_G+|;*9^GK%LT&eKKKLT6Cw%*GJ99bPE zFts3%eMFtRJZL1>JZAksMy%X5hCx5iP%;3Q)m2m~dXUqt~qEEAHrHaNG3Z<^C7P}3we&E~Q z#rf>Ff2%k?dj0dCXaA1<;FO64FBo7&$O7%drSK_?6!JNg@!CezY_opg=YOO9Tb-9a zM%?LW|9Qp@v4yZ#f#k1^dRoR{ZKIK@TH6T$p$W(z&Py^F2O~Jguk7sB52BAPK$mO1 z9$J}lmP?mzl)n~plSndI{;<5TX2G#+xW?0~9q)AsK)VG9fnjlOZ|q5XGLR_^6GXOt zK;r>``1tw(oQB*GTBZ;N7JrK1GGv)$Mxql!+7r0J?^x8_0AX8 zwJmbHX1v2UB)VccB{Ry>2mCn-T4XP~Zm`Mvfkv_SsxsId$ReDdYw4J{1siXGc`db} zPoXF_qnQK<>j!vzhZxihv~jhT4OaPE){eusrMO4-#sbG%IQB&i-hU0h?|lMT1&VZM zhiKAxy!|JC1Fx^(e;)Zq$7wZh%4}9#FfBB~UJglVS7#jMnLP8yL}{N_$v^v@U~{X- zKJmBqM%_;!2R6F<(yZW=UNd14iP!YCP#-iPy7dAXT=(1A@vTQHODcte)cyeCk=$wm zw?%I!4L~ejQmhWkhksy^c5|hZGDDRiW5Y^^noa@`eVE#Yte6W2{ZD+P89ke%iFbM$ z{r0an)5(N-H2&s{T^uEEKzi+w?0L_Y9q+<#e(g`1?X5X`)9mki+xtPc&jy_|P~l98 zFly~jWnVxXh?fHoJkbvmrriAj@t(!Bgm|X5%D7!g<|LBBdht9HL z13e+An?u^714IK?ImnyONlkkGKo|0~-0W-D*KMX(%<_?9m*n#J$0~_}wbmtS*AMIw zg&Uu^4Fo{I27g{~0XBK3g0?;%G^H}cpQN(6)(->~ahU+ngiWY|nq5cmtI#&Pi-HXE zU0v|MGE*Jcssu*WmTZw$$requPaeKlNLQEBTb}vAW$^IJw@mxUiG!%grvoxcRNd+xMzV^(u z4hp`U2dBWK z|MhPKvwvAw91&+}n3lMkhU}JBv(o|}RprT9#qO8?^wi(pzE}0b{k`nN&sE({vB&D_ zs81ADB38_y0M?zUZCNVy30G4Zl(JMq&(uyW<1`*mU0ATM8tM&WAq^S?NFrshrLdkz zmD4-U?nf;F>iQ9t>c?KVx4OU5akB^jkeE&jz<;l_egFWg8w6jdqVg`-|F~lHJ;L6f z5_Op!%(ef3$pP{f_JYkqXMP<&=cgT&StoI;I@k{vmiXa|_VLsGSroR&)#0BChsoOr zin%b2O8aKhQnaPWdyOq_SwswKX&(mozC%YrkJ=+67%UA(WE~zJ<5A~5yNR0PRa>AZ zmVZ{Pv&!8dfG!8A!3BI~U$}5GKraO)a&Yxisz`~N69KG{8DSIYUUI3!}*6qh2R(B=v*dmhe^2;CvO)=kS_ z`5k{}Cr-D$Tl@J(0`>(=dYi}r)>jFkrGM!<#^gGU4p%>#=x+%_i}3BCI?(JhPWQ(Y z&kNQs>ueovj1M$1Ot2@bVuGduIov2h>xqd+t+GK;jGWY}^24mN!~2?*!rN8v?`~ry zn@;B5;bH0zo034=AUOY+ZMTiv}LzYiC69()uIl0yaW@T0Ne*6kgjNo4k{Zl5+|KQ+G&-dy-yT z8aHxqAaU33xf>5GT1x6G9XE>rS`-o_-Q_#3AD}Y;J&C2#+CCChGr|_cP6bCo=ZW^D zsgf>k$RVbo<*_Vj`Q*(nzzkkc8-GK18~M7feY5072CB?=br}Am(kmYo4kDuZ^Q-j( zA!B_hO67s@{Q z8O8%U$sJT>ZTZWwRP%MCzI9V(CH{AM*)n7hFdV~7t&1IEkkh>IIk)jyViJQ zmBSh6-Sn2U6!CHa08=jfwSV@p)Y}!=TebvIVN>Sr$#PXro%Hy+m`D1kl1eQKX>fn3s#;*MBlH2@EFmlqPWd zj&XIh;}X-l+6V9TR1VlJ%%#rq?=9C4n6RHoe>H40eUwi?X5vl{>Bai=NMlOuLgWkT zGiyp8k=p*d(*k1lR7x-1Z)aWTyXA5D+NF4yP6^%J@?!bjsu$!#i&P0=|Ke-AwYp($ z#9s*NxOGc_di4QB4}XuK;V!0-`vl)Z{#aqmus9x#KwItK131P*R-x9cNM^%UX5j^7 z5f^#T0wFzlwh&krzG$3PM(=gO+ZOV6Yww=+>Amd>v??@!<4WrXg0OS$T@)UhZn1tK zjI>dGNZ@LOc^J%tdX^m>RIk0(4U*(yl-#-lFU8bBFd=sDs(;~QFiqPK8k07m+MyaI zb`5K^nYSL|VUU!ww92|{Wbd&iYn~A9O~wxeYt)hC3;fD0&A8Fr@iOyq%S@{IU3eUf9s+i6mfq%OR|zR zV})1FdtM%TCx6X+>X+cA&OtK{i0)K#D#jUTc-1yU|LFu%zqtE#LNzkNKu>~v$Ed9b%3F( z)MQ3~vS2LKq03y%u1P>=rJ#d^ypYt_ZoyKB>@!Z*r@GP6a!q&h#Aot$H4&mXVsPin z_0uX#b|@qYH|qNx_8*AC*M>M3YOw_3PZ|uc)Hp(wLIn6wbX8{)!RVCuLUnL*>di85 z1F1+T3V*l5yUoo0NBe!;2&hurp(7@uhhq9#sB)N?8xh#{8?Sj$McM#;jRT)Nem5$ zu0w~#8V3dwFR8j|{(~P6x3=zl?6XnYi?lR12!9Aq;;^>iy1wglM47D}aX7z}C(;=!5|d0ua|x@0()4j{xAF zJhFZOpvM21K930q!vGu!GD`wKC?+vWZuE+&1~H4-D}crCG{7Np@#$@ zPDzQJDcZ!h)~^#+uoMCKou0ZJe^t(LSC)9tNnFrb!S$KNaL_Ylw8CMHyFz+FgVspl zAT?i6RBU#kZR*|J#(r7)uO}Ljk3e^tVoq|}SindVrG^#PY;ZUo2gF}Fj{GUU5`W#M z+wmDcuzw|nA==(CnAjj#18;F5cP5rmfGq)_HJdZe{v2@%hhid@^1Rj}JSvpoOZ|40PIX?4mH zfs2x$i*;YhX}H~BJ7>`uL*Qms5`Sp1>Vq_a&)h1iwJ$$1FkE>maFM7-L0Mb`X`28A zxn=(eCR&9R-+dcxWd(PBL}4_2qxT;N*?(q#kNe?U|)?-2bcE04d_oE?|H_<#9Fr+@uF|CW~( zbD3$H#Jqhm@8}YOjiGGC>%0y(o9w?C!;0s|;O zG{r7OT92o)#195dr|Wk+p7uM9%pFMt z)z%VXP?n2o>yjEMXQA`rP$5rp7@b))?L-#xo-(^og%1eU9KU}|q<@VjXMit@KgrPf z!GD{^V4yEdj?I-&a|pB@u?u8~J)_K=1W@H9(?m9aP5Y<-n3vX8y&?d_(2^RHK%TPK@@xs3;%9jVDMXgn@` z_WYyEL(YTm)aPfj&wZ{sIi3B*mqF0pcp7ScMDyQSCF0(QQpE77JxBuIB_6|3+5^eGc1P$Dc1hYZ6ZC+x3>J zHX34Xo_#9SX(A^Z52*8mk96H4BiE^mJN+8cUwR-tTDA_^L{T*6j6GSlQWB)9IcEJ@ z$qp_OIU#Wl*%G!y*keyoZ(AP7fz}UFMgaS7q}3 z;$(GK&ZW9?>{(l`bEYRNujdjcB2Jt*aUym_IDc{nCF%rgu>}JVcbo*1IGj{NmKf66 zVsSaV`k(iw|M|mKt(gob^Llkjq(@wQvVBp$pusPRLRIqtZ@g0hhK+?nRuQ4Y;Gvpl z?_48Z1exHhOm)dgnk+ETMgXmyYtp_7YXlezYrWoLz6hJm^ztEMA!82BVfO%^V@wU! z%YSe>#OZ)W6~Z@@N6b4K0W>lQv^yl4$T|S@Gz^xL$@qu=^@EG=eDAz>-kPk~!Ur^F z&6tsfdP28|r?x;-j;EJcfLa!GCN;gm+0c_;Ldu>u^;K8u~;Q29(TI&Ia9m zyWSnmEU)}=tc7P1hBOy#8(sgc=$>`IE;hO{eOCT z|NiI){~OFFeJOxKq7gFSqC1VmqUHn%1s|1gz^m zT!r!FH2ZpRc+h2o?x3H3w1qf?;s`Q8=OTf}X4zn;eJY#-5rlBaZ={k~!F-&LY|8+@ zWUm!le%K~KQ7HeTS|z!6kNEbz!GFo>{)5Z!eH)8ugf*r`B4Ieb?E8eG)B!Cf7vL{> z03uO)srn!dnNlH~$fFM>b)+G239`0D3YNBw0_ild~0F*Zbqhl+C9^S&nl zfUq@*r?J)#rpMKTb}*ZyHMS9=cXLb+;-MCV)?a}vE`L)fNtXCr zE^S2$yCaa9RXGa4)&iZ3&=mIXu*r^=NddbG7f%$s<#O{gS2G=*-T+xZroRaxL*+6d zsZ=YSRFxs5h_(|@WuED=C|Y@FaDpry-s^#=sdN(kSbh``FH>Z#@bVE#ht$*hr!Q zV}DV?vC{2Org#-qNKaRtJnc_U@~4b=+(KDdtwp#?suOO85otX&aryCrNrqf83|Jn8 zf_A)+b^)SN2atz2J#NqxC+cb#G~(d#6`c|!sENV;7fGG~wm}V!lE{B>CqVN8^tzCrm-C2)K=o1J0{_i7Kj6u%wXQw=7UF7!kV%39ut0I z+_&lXG-h{N8nz#f7!lt6=Whk)XRn?e6AMz5!mPuP3mgQ^5Sc_dS{UW7OzTP~gc_`{8nE)q-FLz|?9!tQ`BtU-xkFt?HfpAA?qBG5j z;eY79l|8sLU(A^W!ujU&5YN5{1jc&xs@StUSD!}MwnL06dR#DI+9~0Gz=E_$^m?q2RYBF2iXHB|- z^c6O>U0;QiOrC!LwgL=z*5vxHDW(kmWx*tj>aDcVywX5CZA$OX;;ay2AzWmMrbJ>$ zm#+kDBx^MmHIa8gmHLQ1vF6twI~&FX&t#(UZAUT}1)7=iD0;arbk*GX3z`%c&llzP@LVKOcHVKSdw%S>% ztqF)6Okxp%wiv~c%Yj!Ij6@h@Dq=Q3>Eqp!zS0QCk}peN!N`Im&ujAO-XsX(_0{N` z!{GQN`@$cv-z*)l1;=W(TjuU9PXKp8%RnjwB+()CIrBO^N{d~~aUqZFFMq|ZcyFhq z5B09&`6z!8$&W4^Rg!Uj9jq4l)lO9? zykuYJcdUG!TLEqiblGowqLl$E?dCrq{_P_%d#et4I@ZiXvf3=-_U${)Ob8Pd0To5M z6-AMVkvikE+Mmz+otidaac1x>AuGT(~kz+akC z;V#j}z*sq#R%e^1w_-M1uAD793;v*s)4(8lv%*lH!|sBPTvD*YDh#{ECO!)9VJ9$~ zjKA|A_%e!5?_nYaYZk?bO5C4YjdmN|x0L+sNBK~qyfBR>V@dIlxfy|D<-=EQLJGO! zgL;3tU{^p~(bQwkc4$?CZ}`*h7wV+x0KYo0oI;+?Pm!*7NPK4oRxkq8&{{P|U^I)P zY|@tZp;xme+PUk0{HO5j$KCVSqXwmx8}d8>+|>_~U&WPq4^aeUL;GVWp?(GVKnW3n zhG`wG!*#M#LR7Fxr*hZ^ly*0y}yo$LyB)VAd9=XQM6I*QQ`Jd(-4NffZ$p=!x)7-y`#4 z@53E1Eg256Kf8GQZ8jYC(v0m>7>rRvw%hW{Jc5!z6qWDPE+u=|&0z>JPM%Ja<3uvh zmu8{vQpVVWpaPRJlHCRnETWkT_anegVBP&C;emB50uRuI)6v@+<*h%BvE_UR zcC|C+=jGpiz?X(-%0ZN7=%S-acYp4O4T}UbBT7XuncMDMVZ4(4LO;B2SjvCve0Rf; z?VX(uPc-C-%gt;+NrEmwakc=anrbjmwc*vaU~SFOJvD1fPMf*r=e(`20IVpCANEs7 zB}$U3A?+uiTiC-tgA9lODj%ep6qP8YB!75DC4ZT30m~`M&mcHnR556I5a2a=-ioUj zgjJ@CV6s?%afDw*F_#jhMl64~9Fya1+azWaYeQK1i&j34TVCUIRsg22C1Jx1-)mqc zUp>1dh<&;mk5{d65VCui+LyTml_bmQX_b0MwS-B*W5m za>i#>MTg0<>+XC!`hJUXVbxm<8N6y)hk!KWxPffeUa!|zAYd;hCV|0_SOHnop4Cwr;~fcl7}sdyvbG#XvsQ%RU?mO?4|5}^?K!WXdIDHO z;yJ%!TZREl)5W~*+iP>WLdmTe(KBHn=m|gp&uIo@@E=hBnz>H zh8Rv2{P5(9&{jwLC?9_gT%NaCzTnwLrJj+4*f}*q2gFo>S2gZfm8qvrO$_6{H9^nq zj1#NrGctVUEeC%8`gpIXNbc7>Sb~n> zn+!BSFtf*$XNnh;fOf4SG(TJ7Cur6d8AJqN7Mf~LWGdNRVAGO-$m7M5J`{k-0kXkt z3akh7(M3#=fLMuU0-{Y3t7y&|$(#+Tj1i@Z@&vFQTx)J@JQ~)E3@TIy$Iyq0A*q*L z|1u)O?q0aBx7UBeJ*E4>bB<^{a;>}gu%)L8e(J_9C6s=L#sd?#Qfotk1|gxT(B5jQ zgQ&6`Vp>IjvulM-v`tB7x#(cZUqG%GC`*gz`m^s!96SMR1D3#Pe4P%3(RSiy7N?3Y z$;RC#O2~x3>ekW9L~Kd}{!2_`-V&7sGXfK(I8=e&Lc)Js>Q|G$wNQqi6~+nnsyRcB zmsTTMyr@U)P3~f>v4j&yam!SNDz@hu4@XR$fKY@tX`Yqk&Z@VCi3ye#I6?lV(FnnR z*f(l4*P2N!q#boDVmwJUG977&273ay6Ii!Q#l#Edn{tc@9`eBCUew`Ydbz9%!+U9o za!k6BfDL~-8M$e(O358{lAo;~*wdGEpe(HT*ci$vU>iHd1jPAm8X%@10fl=4iUFoP zbPS;6$)D+X$jwHXyh&VYWd_oK>NjTUJGR&pz&1buT(@IFmcERyXnvNfq}yLj=9lwD zHl8&wLg#^&DYZyOS~KBo+5R-oVito4`HFj~I+1@{D^X1OAeF>!P%TF<)mv7aKTScq ze2dqH48<^IF_;Ovr?ku+kjIw533(-OOq@jZm&*nIa!^D6hDUfw#2t2IK z*Z>eMl3EBf@Jrcj6>YNbF3%Id9WW~Nn(!+OlQ>Ljm}oH+H#HjTpZ;E~hwmuA_*hdA z)d7FA0{qE|<{Y0_>Gq~Qwn4z16+i!3UkL|AhcPlbc1dExQWjWc)}lWMye^iLvbD;I z7}>`N!CL0B%X8vBPKba;jpa$kqOwJJ0$BIXbAG`xd!k;fp=L(+eNGUeY{*WfYS6_w zfqXM4$%@7h^rpk@_RsjRKnSW-W`j;}*vWt7da)X7!va>L7_35qT0_Wj&efwV%9W}w zu0W4ZSIhJv)`6fs4t*j_m(6s^R0Zf~7!w(qkQJALQ@PskHnt{wPK%_0!{F3ORAdBA zh~C&Z9j}gi!AZZ->W1=LQ?3Y40K0cMkcHo{OTM1f>oV=ahnc{A!{W+;e)ocJ-;{sK zr9!W#Yze3*GS5+&x8>T|4Wqp|xy|5bdEc&1_`qfF)f+1%2Um(qGhVMepmVhJ~nl)*gxkGgoYR>5a$4$CeVSPg!hJh)oAxys#a<#P;lBO53CEXFe0$Kb`#b zyLi%rLqG$Qn(SEd9gW(C*f2=(SPAWr<&&>y27shvV$Teh-V7;BgMxpsLkOKpIAL#U zrx|?e&G6`KwyH&wAi*lB3>A0+coO1ITstXSNNr%{M0=p{ivkaZ=gAGn#-0Fu%zH15 z1|TuX+)1_F30v47*a!ewkP2l|hcZ}T8NAp5uFTxG+B*mq>=w0UC`pUr)2fmK9~a!q zYgDM{orN;M=4~_SpZ9;oW;F020ipIGfH#ru+XTDUgK~MRa8Cf+yFknos1h;Qy+>Ed zgMKYLeEsz(sZZD+fYhsqFL0@sJ_1D5YWMur=`~aVEPx`bc9Doa5h^c3ZKzY-57s1MF$K8FHkE+UST!k z0KK4g!GHv6v_H^Yy*J2?j?&j&3;V})ML;f{JOSJRGT^Qbu!FR5I;T4;#IuMx?X-P> z&q{eEz#38g&Ul~>%X`8^(#>9RGOE32*>K1$iL8KpxX~9N4Ed0$&Tket2`4l-j0Q(h z(#)!f4L$A~Rf~TY*F12uO!w%e9bv>UW2WQHW~I{&rvy|TVwj+cJKR;Zm=;3-yr~Jg zD!Ngfb)9wVW|**ZE;BsF^R2#K-@@jDfreloQ7w)R4l?|wr(HmK-s>@~f+Zd%+)0vj^`l|Mq{+!^fA=>J*fyYNy8bjg2+*8n#5vZuTvUcqi147N2M2rih{yRse*M8i(S?&E%QB? zE-Oa|%|HH=%E>vQgxDo{V2jTot8plMuuc^MIrK6FD;H3B8yueZjKWs<#K1#K)T*?P zJ74}=LF}F5b0$yN@9#Ut9ox2TZoIK=Z){r|JK5OD z#@5C*Hny{|&6D4Yr_LX6UY)9Es=B&ou4|^Ix@M-Y?&DW0~ilv{r zoJ?(-7gIpvZ_K1YEzWE7xWUJ-G9R;(;0&#d^F*n9zQJ*j4*(69<+u8 z9B6;~=S5UQ>uJSIuWbEdb6KgFl}E^BpX!w)f>T$QbKhsd@7168t*X2p9`1maD?$c+ z`%-MYtATpAW9Ei=3Es}bihP21Y4qL`0NMm RERW>y|LzsoMtq_1Z$Ye}rtx9WVB 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%0D}NNTOb=ClLeUgx2#HH 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 xM#bgtEsc!GM1zx%)Ce*oX|r|kd$ delta 204147 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%0D=I#~n7R&pY3rJ1 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

2;#xMt~!K9g!kDh$igTmgo#0{;z@lXz=dv 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 Bp1S}5 From f3ba5e94b956c964067b3d10badef91eb3cdc1a7 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 15 Jan 2026 15:00:59 +0500 Subject: [PATCH 009/405] commit: update wheels windows installer script for default template --- tools/installer/windows/install-wheels.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/installer/windows/install-wheels.ps1 b/tools/installer/windows/install-wheels.ps1 index bc0fcbf08b..c772fb6b8a 100644 --- a/tools/installer/windows/install-wheels.ps1 +++ b/tools/installer/windows/install-wheels.ps1 @@ -62,7 +62,7 @@ param( [string]$AppName = "MyWheelsApp", [Parameter()] - [string]$Template = "wheels-base-template@BE", + [string]$Template = "wheels-base-template@^3.0.0", [Parameter()] [string]$ReloadPassword = "changeMe", From eb36f94f45f7888303ed88b47c3facd40fb3ddfb Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 15 Jan 2026 18:29:41 +0500 Subject: [PATCH 010/405] commit: update wheels macOS installer with new fixes --- tools/installer/macos/install-wheels | 23 ++++-------------- .../macos/installer/wheels-installer.dmg | Bin 376978 -> 376865 bytes 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/tools/installer/macos/install-wheels b/tools/installer/macos/install-wheels index f73f555f9b..29615727b8 100755 --- a/tools/installer/macos/install-wheels +++ b/tools/installer/macos/install-wheels @@ -198,25 +198,14 @@ check_existing_installations() { if servers_json=$("$box_path" server list --json 2>/dev/null); then if [[ -n "$servers_json" ]]; then local existing_server - existing_server=$(echo "$servers_json" | grep -o '"name":"'"$APP_NAME"'"' || true) + existing_server=$(echo "$servers_json" | grep -oi '"name":"'"$APP_NAME"'"' || true) if [[ -n "$existing_server" ]]; then - local server_details="Name: $APP_NAME" - local weburl - weburl=$(echo "$servers_json" | grep -o '"weburl":"[^"]*"' | head -1 | cut -d'"' -f4 || true) - local webroot - webroot=$(echo "$servers_json" | grep -o '"webroot":"[^"]*"' | head -1 | cut -d'"' -f4 || true) - - if [[ -n "$weburl" ]]; then - server_details+="\nURL: $weburl" - fi - if [[ -n "$webroot" ]]; then - server_details+="\nWebroot: $webroot" - fi - stop_with_error "A CommandBox server already exists with this name: $APP_NAME" fi fi + else + stop_with_error "Failed to check existing servers Application Name: $APP_NAME" fi fi log_success "No conflicting installations found" @@ -287,9 +276,7 @@ check_java() { install_dependencies_via_terminal "$homebrew_needed" "$java_needed" # Wait for installation completion - local counter=0 while true; do - counter=$((counter+1)) if [[ -f "$INSTALL_FLAG" ]]; then local result result=$(cat "$INSTALL_FLAG") @@ -308,7 +295,7 @@ check_java() { stop_with_error "Dependency installation failed" fi else - log_info "Waiting for dependency installation… (${counter} checks so far)" + log_info "Waiting for dependency installation……" fi sleep 2 done @@ -407,7 +394,7 @@ if ! grep -q "openjdk@${MINIMUM_JAVA_VERSION}/bin" "\$SHELL_PROFILE"; then echo "export PATH=\"\$JAVA_PREFIX/bin:\$PATH\"" >> "\$SHELL_PROFILE" fi -# Verify Java +# Verify Java (real JVM check) if command -v java >/dev/null 2>&1; then echo "" echo "Java installed successfully." diff --git a/tools/installer/macos/installer/wheels-installer.dmg b/tools/installer/macos/installer/wheels-installer.dmg index 4e9567a2fab98b6281a6536389c26d400a3875a3..6d1ba3639b9a8d0d6ef629062deb1e420506dff5 100644 GIT binary patch delta 169994 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 T3*V<8JH8!2i*EbD__z8eQrT6c delta 170581 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

Rvp!K>G3v(j@9~H9^7S})G9+$74`M8Q+aVc=Lh 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%0AU)RjCLZwJx;*kaS<&wrfLSO5(iM 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%0A%R;A~u%vk_{iubd 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 M48JaC<^4tf0slj%AOHXW From daa60b2a0dd6f0e6499a0f41156a2e78bedf3b6c Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 16 Jan 2026 19:37:17 +0500 Subject: [PATCH 011/405] cli/bug fixes - Removed invalid calls to detailOutputService's functions - Improved error handling in multiple commands. --- cli/src/commands/wheels/analyze/code.cfc | 137 +++++----- .../commands/wheels/analyze/performance.cfc | 199 +++++++------- cli/src/commands/wheels/config/check.cfc | 256 +++++++++--------- cli/src/commands/wheels/config/diff.cfc | 213 ++++++++------- cli/src/commands/wheels/env/list.cfc | 59 ++-- cli/src/models/EnvironmentService.cfc | 45 +-- .../commands/environment/env-list.md | 1 - 7 files changed, 447 insertions(+), 463 deletions(-) diff --git a/cli/src/commands/wheels/analyze/code.cfc b/cli/src/commands/wheels/analyze/code.cfc index f702d1b144..738314996a 100644 --- a/cli/src/commands/wheels/analyze/code.cfc +++ b/cli/src/commands/wheels/analyze/code.cfc @@ -28,80 +28,85 @@ component extends="../base" { boolean report = false, boolean verbose = false ) { - requireWheelsApp(getCWD()); - // Reconstruct and validate arguments with allowed values - arguments = reconstructArgs( - argStruct = arguments, - allowedValues = { - format: ["console", "json", "junit"], - severity: ["info", "warning", "error"] - } - ); - - // Set verbose mode if requested - if (arguments.verbose) { - print.setVerbose(true); - } - - if (arguments.verbose) { - print.yellowLine("Analyzing code quality with verbose output...").toConsole(); - detailOutput.line(); - detailOutput.output("Configuration:"); - detailOutput.output(" Path: #resolvePath(arguments.path)#"); - detailOutput.output(" Severity filter: #arguments.severity#"); - detailOutput.output(" Fix mode: #(arguments.fix ? 'enabled' : 'disabled')#"); - detailOutput.output(" Output format: #arguments.format#"); - detailOutput.output(" Report generation: #(arguments.report ? 'enabled' : 'disabled')#"); - detailOutput.line(); - } else { - print.yellowLine("Analyzing code quality...").toConsole(); - detailOutput.line(); - } - - // Pass the print object to the service - var results = analysisService.analyze( - path = resolvePath(arguments.path), - severity = arguments.severity, - printer = print, - verbose = arguments.verbose // Pass verbose flag to service - ); - - if (arguments.fix) { - detailOutput.line(); - print.yellowLine("Applying automatic fixes...").toConsole(); - var fixed = analysisService.autoFix(results, print); // Pass print here too - print.greenLine("Fixed #fixed.count# issues automatically").toConsole(); - - if (arguments.verbose && arrayLen(fixed.files) > 0) { - detailOutput.output("Files modified:"); - for (var file in fixed.files) { - detailOutput.output(" * #file#"); + try{ + requireWheelsApp(getCWD()); + // Reconstruct and validate arguments with allowed values + arguments = reconstructArgs( + argStruct = arguments, + allowedValues = { + format: ["console", "json", "junit"], + severity: ["info", "warning", "error"] } + ); + + // Set verbose mode if requested + if (arguments.verbose) { + print.setVerbose(true); } - detailOutput.line(); - // Re-analyze after fixes if (arguments.verbose) { - print.yellowLine("Re-analyzing after fixes with verbose output...").toConsole(); + print.yellowLine("Analyzing code quality with verbose output...").toConsole(); + detailOutput.line(); + detailOutput.output("Configuration:"); + detailOutput.output("Path: #resolvePath(arguments.path)#", true); + detailOutput.output("Severity filter: #arguments.severity#", true); + detailOutput.output("Fix mode: #(arguments.fix ? 'enabled' : 'disabled')#", true); + detailOutput.output("Output format: #arguments.format#", true); + detailOutput.output("Report generation: #(arguments.report ? 'enabled' : 'disabled')#", true); + detailOutput.line(); } else { - print.yellowLine("Re-analyzing after fixes...").toConsole(); + print.yellowLine("Analyzing code quality...").toConsole(); + detailOutput.line(); } - results = analysisService.analyze( + + // Pass the print object to the service + var results = analysisService.analyze( path = resolvePath(arguments.path), severity = arguments.severity, printer = print, - verbose = arguments.verbose + verbose = arguments.verbose // Pass verbose flag to service ); - } + + if (arguments.fix) { + detailOutput.line(); + print.yellowLine("Applying automatic fixes...").toConsole(); + var fixed = analysisService.autoFix(results, print); // Pass print here too + print.greenLine("Fixed #fixed.count# issues automatically").toConsole(); + + if (arguments.verbose && arrayLen(fixed.files) > 0) { + detailOutput.output("Files modified:"); + for (var file in fixed.files) { + detailOutput.output("* #file#", true); + } + } + detailOutput.line(); - detailOutput.line(); - displayResults(results, arguments.format, arguments.severity); - - if (arguments.report) { - generateReport(results); - } - - if (results.hasErrors) { + // Re-analyze after fixes + if (arguments.verbose) { + print.yellowLine("Re-analyzing after fixes with verbose output...").toConsole(); + } else { + print.yellowLine("Re-analyzing after fixes...").toConsole(); + } + results = analysisService.analyze( + path = resolvePath(arguments.path), + severity = arguments.severity, + printer = print, + verbose = arguments.verbose + ); + } + + detailOutput.line(); + displayResults(results, arguments.format, arguments.severity); + + if (arguments.report) { + generateReport(results); + } + + if (results.hasErrors) { + setExitCode(1); + } + } catch (any e) { + detailOutput.error("#e.message#"); setExitCode(1); } } @@ -179,7 +184,7 @@ component extends="../base" { var fileIssues = results.files[filePath]; var relativePath = replace(filePath, getCWD(), ""); - print.boldLine("#relativePath# (#arrayLen(fileIssues)# issues)"); + print.boldLine("#relativePath# (#arrayLen(fileIssues)# issues)").toConsole(); // Group issues by severity for better readability var groupedIssues = groupIssuesBySeverity(fileIssues); @@ -190,7 +195,7 @@ component extends="../base" { var icon = getSeverityIcon(issue.severity); var color = getSeverityColor(issue.severity); - detailOutput.output(" #icon# Line #issue.line#:#issue.column# - #issue.message#"); + detailOutput.output("#icon# Line #issue.line#:#issue.column# - #issue.message#", true); print.cyanLine(" Rule: #issue.rule#" & (issue.fixable ? " [Auto-fixable]" : "")).toConsole(); } } @@ -216,7 +221,7 @@ component extends="../base" { detailOutput.header("CODE QUALITY REPORT"); // Display grade with appropriate color - print.redBoldLine(" Grade: #grade# (#score#/100)"); + print.redBoldLine(" Grade: #grade# (#score#/100)").toConsole(); // Display grade description var gradeDesc = getGradeDescription(grade); diff --git a/cli/src/commands/wheels/analyze/performance.cfc b/cli/src/commands/wheels/analyze/performance.cfc index e4be90110a..a6551861bf 100644 --- a/cli/src/commands/wheels/analyze/performance.cfc +++ b/cli/src/commands/wheels/analyze/performance.cfc @@ -24,117 +24,122 @@ component extends="../base" { numeric threshold = 100, boolean profile = false ) { - // Validate we're in a Wheels project - requireWheelsApp(getCWD()); - // Reconstruct and validate arguments with allowed values - arguments = reconstructArgs( - argStruct = arguments, - allowedValues = { - target: ["all", "controller", "view", "query", "memory"] - }, - numericRanges={ - duration:{min:1, max:1000}, - threshold:{min:1, max:5000} - } - ); + try{ + // Validate we're in a Wheels project + requireWheelsApp(getCWD()); + // Reconstruct and validate arguments with allowed values + arguments = reconstructArgs( + argStruct = arguments, + allowedValues = { + target: ["all", "controller", "view", "query", "memory"] + }, + numericRanges={ + duration:{min:1, max:1000}, + threshold:{min:1, max:5000} + } + ); - print.yellowLine("Analyzing application performance...").toConsole(); - detailOutput.line(); + print.yellowLine("Analyzing application performance...").toConsole(); + detailOutput.line(); - var results = { - startTime = now(), - endTime = dateAdd("s", arguments.duration, now()), - target = arguments.target, - threshold = arguments.threshold, - metrics = { - requests = [], - queries = [], - views = [], - memory = [] - }, - summary = { - totalRequests = 0, - avgResponseTime = 0, - maxResponseTime = 0, - minResponseTime = 999999, - slowRequests = 0, - totalQueries = 0, - avgQueryTime = 0, - slowQueries = 0, - memoryUsage = { - avg = 0, - max = 0 + var results = { + startTime = now(), + endTime = dateAdd("s", arguments.duration, now()), + target = arguments.target, + threshold = arguments.threshold, + metrics = { + requests = [], + queries = [], + views = [], + memory = [] + }, + summary = { + totalRequests = 0, + avgResponseTime = 0, + maxResponseTime = 0, + minResponseTime = 999999, + slowRequests = 0, + totalQueries = 0, + avgQueryTime = 0, + slowQueries = 0, + memoryUsage = { + avg = 0, + max = 0 + } } + }; + + if (arguments.profile) { + enableProfiling(); } - }; - - if (arguments.profile) { - enableProfiling(); - } - - // Start monitoring - detailOutput.output("Starting performance monitoring for #arguments.duration# seconds..."); - detailOutput.output("Target: #arguments.target#"); - detailOutput.output("Threshold: #arguments.threshold#ms"); - detailOutput.line(); + + // Start monitoring + detailOutput.output("Starting performance monitoring for #arguments.duration# seconds..."); + detailOutput.output("Target: #arguments.target#"); + detailOutput.output("Threshold: #arguments.threshold#ms"); + detailOutput.line(); - // Monitor performance - var progress = 0; - var spinner = ["|", "/", "-", "\"]; - var spinIndex = 1; + // Monitor performance + var progress = 0; + var spinner = ["|", "/", "-", "\"]; + var spinIndex = 1; - while (now() < results.endTime) { - var currentProgress = int((dateDiff("s", results.startTime, now()) / arguments.duration) * 100); + while (now() < results.endTime) { + var currentProgress = int((dateDiff("s", results.startTime, now()) / arguments.duration) * 100); - if (currentProgress > progress) { - progress = currentProgress; + if (currentProgress > progress) { + progress = currentProgress; - var spinChar = spinner[spinIndex]; - spinIndex = spinIndex == arrayLen(spinner) ? 1 : spinIndex + 1; + var spinChar = spinner[spinIndex]; + spinIndex = spinIndex == arrayLen(spinner) ? 1 : spinIndex + 1; - var bar = repeatString("=", int(progress / 5)) & repeatString(" ", 20 - int(progress / 5)); - var progressStr = "[#bar#] #progress#% #spinChar# Monitoring..."; + var bar = repeatString("=", int(progress / 5)) & repeatString(" ", 20 - int(progress / 5)); + var progressStr = "[#bar#] #progress#% #spinChar# Monitoring..."; - // Print progress on same line - print.text(chr(13) & progressStr).toConsole(); + // Print progress on same line + print.text(chr(13) & progressStr).toConsole(); - // Collect metrics based on target - if (arguments.target == "all" || arguments.target == "controller") { - collectControllerMetrics(results); - } - if (arguments.target == "all" || arguments.target == "query") { - collectQueryMetrics(results); - } - if (arguments.target == "all" || arguments.target == "view") { - collectViewMetrics(results); - } - if (arguments.target == "all" || arguments.target == "memory") { - collectMemoryMetrics(results); + // Collect metrics based on target + if (arguments.target == "all" || arguments.target == "controller") { + collectControllerMetrics(results); + } + if (arguments.target == "all" || arguments.target == "query") { + collectQueryMetrics(results); + } + if (arguments.target == "all" || arguments.target == "view") { + collectViewMetrics(results); + } + if (arguments.target == "all" || arguments.target == "memory") { + collectMemoryMetrics(results); + } } - } - sleep(1000); // Check every second - } + sleep(1000); // Check every second + } - // Clear the progress line and show completion - print.line(chr(13) & "[" & repeatString("=", 20) & "] 100% Complete! ").toConsole(); - - if (arguments.profile) { - disableProfiling(); - } - - // Calculate summary statistics - calculateSummary(results); - - // Display results - displayResults(results); - - if (arguments.report) { - generatePerformanceReport(results); - } - - // Exit with error if performance issues found - if (results.summary.slowRequests > 0 || results.summary.slowQueries > 0) { + // Clear the progress line and show completion + print.line(chr(13) & "[" & repeatString("=", 20) & "] 100% Complete! ").toConsole(); + + if (arguments.profile) { + disableProfiling(); + } + + // Calculate summary statistics + calculateSummary(results); + + // Display results + displayResults(results); + + if (arguments.report) { + generatePerformanceReport(results); + } + + // Exit with error if performance issues found + if (results.summary.slowRequests > 0 || results.summary.slowQueries > 0) { + setExitCode(1); + } + } catch (any e) { + detailOutput.error("#e.message#"); setExitCode(1); } } diff --git a/cli/src/commands/wheels/config/check.cfc b/cli/src/commands/wheels/config/check.cfc index c247e94e0c..b5d4d8104f 100644 --- a/cli/src/commands/wheels/config/check.cfc +++ b/cli/src/commands/wheels/config/check.cfc @@ -22,141 +22,147 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { boolean verbose = false, boolean fix = false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs(argStruct=arguments); - // Determine environment - local.env = Len(arguments.environment) ? arguments.environment : getEnvironment(); - - detailOutput.line(); - detailOutput.header("Configuration Validation - #local.env# Environment"); - detailOutput.line(); - - local.issues = []; - local.warnings = []; - local.fixed = []; + try{ + requireWheelsApp(getCWD()); + arguments = reconstructArgs(argStruct=arguments); + // Determine environment + local.env = Len(arguments.environment) ? arguments.environment : getEnvironment(); + + detailOutput.line(); + detailOutput.header("Configuration Validation - #local.env# Environment"); + detailOutput.line(); - // Check settings files exist - if(arguments.verbose) { - print.line("Checking configuration files... ").toConsole(); - } - local.configPath = ResolvePath("config"); - local.settingsFile = local.configPath & "/settings.cfm"; - local.envSettingsFile = local.configPath & "/" & local.env & "/settings.cfm"; + local.issues = []; + local.warnings = []; + local.fixed = []; - if (!FileExists(local.settingsFile)) { - detailOutput.statusFailed("Files Configuration"); - ArrayAppend(local.issues, { - type: "error", - message: "Missing config/settings.cfm file", - fix: "Create a settings.cfm file in the config directory" - }); - } else { - detailOutput.statusSuccess("Files Configuration"); - } + // Check settings files exist + if(arguments.verbose) { + print.line("Checking configuration files... ").toConsole(); + } + local.configPath = ResolvePath("config"); + local.settingsFile = local.configPath & "/settings.cfm"; + local.envSettingsFile = local.configPath & "/" & local.env & "/settings.cfm"; - // Load configuration - local.config = {}; - if (FileExists(local.settingsFile)) { - local.config = loadConfiguration(local.settingsFile, local.envSettingsFile); - } + if (!FileExists(local.settingsFile)) { + detailOutput.statusFailed("Files Configuration"); + ArrayAppend(local.issues, { + type: "error", + message: "Missing config/settings.cfm file", + fix: "Create a settings.cfm file in the config directory" + }); + } else { + detailOutput.statusSuccess("Files Configuration"); + } - // Check for required settings - if(arguments.verbose) { - print.line("Checking required settings... ").toConsole(); - } - local.startCount = ArrayLen(local.issues); - checkRequiredSettings(local.config, local.issues, local.warnings); - if (ArrayLen(local.issues) > local.startCount) { - detailOutput.statusFailed("Required Settings"); - } else { - detailOutput.statusSuccess("Required Settings"); - } + // Load configuration + local.config = {}; + if (FileExists(local.settingsFile)) { + local.config = loadConfiguration(local.settingsFile, local.envSettingsFile); + } - // Check for security issues - if(arguments.verbose) { - print.line("Checking security configuration... ").toConsole(); - } - local.startCount = ArrayLen(local.issues); - local.startWarnings = ArrayLen(local.warnings); - checkSecuritySettings(local.config, local.issues, local.warnings, local.env); - if (ArrayLen(local.issues) > local.startCount) { - detailOutput.statusFailed("Security Configuration"); - } else if (ArrayLen(local.warnings) > local.startWarnings) { - detailOutput.statusWarning(); - } else { - detailOutput.statusSuccess("Security Configuration"); - } + // Check for required settings + if(arguments.verbose) { + print.line("Checking required settings... ").toConsole(); + } + local.startCount = ArrayLen(local.issues); + checkRequiredSettings(local.config, local.issues, local.warnings); + if (ArrayLen(local.issues) > local.startCount) { + detailOutput.statusFailed("Required Settings"); + } else { + detailOutput.statusSuccess("Required Settings"); + } - // Check database configuration - if(arguments.verbose) { - print.line("Checking database configuration... ").toConsole(); - } - local.startCount = ArrayLen(local.issues); - local.startWarnings = ArrayLen(local.warnings); - checkDatabaseSettings(local.config, local.issues, local.warnings); - if (ArrayLen(local.issues) > local.startCount) { - detailOutput.statusFailed("Database Configuration"); - } else if (ArrayLen(local.warnings) > local.startWarnings) { - detailOutput.statusWarning(); - } else { - detailOutput.statusSuccess("Database Configuration"); - } + // Check for security issues + if(arguments.verbose) { + print.line("Checking security configuration... ").toConsole(); + } + local.startCount = ArrayLen(local.issues); + local.startWarnings = ArrayLen(local.warnings); + checkSecuritySettings(local.config, local.issues, local.warnings, local.env); + if (ArrayLen(local.issues) > local.startCount) { + detailOutput.statusFailed("Security Configuration"); + } else if (ArrayLen(local.warnings) > local.startWarnings) { + detailOutput.statusWarning("Security Configuration"); + } else { + detailOutput.statusSuccess("Security Configuration"); + } - // Check environment configuration - if(arguments.verbose) { - print.line("Checking environment-specific settings... ").toConsole(); - } - local.startWarnings = ArrayLen(local.warnings); - checkEnvironmentSettings(local.config, local.issues, local.warnings, local.env); - if (ArrayLen(local.warnings) > local.startWarnings) { - detailOutput.statusWarning(); - } else { - detailOutput.statusSuccess("Environment-Specific Settings"); - } + // Check database configuration + if(arguments.verbose) { + print.line("Checking database configuration... ").toConsole(); + } + local.startCount = ArrayLen(local.issues); + local.startWarnings = ArrayLen(local.warnings); + checkDatabaseSettings(local.config, local.issues, local.warnings); + if (ArrayLen(local.issues) > local.startCount) { + detailOutput.statusFailed("Database Configuration"); + } else if (ArrayLen(local.warnings) > local.startWarnings) { + detailOutput.statusWarning("Database Configuration"); + } else { + detailOutput.statusSuccess("Database Configuration"); + } - // Check .env file - if(arguments.verbose) { - print.line("Checking .env file configuration... ").toConsole(); - } - local.startCount = ArrayLen(local.issues); - local.startWarnings = ArrayLen(local.warnings); - local.startFixed = ArrayLen(local.fixed); - checkEnvFile(local.issues, local.warnings, arguments.fix, local.fixed); - if (ArrayLen(local.fixed) > local.startFixed) { - detailOutput.statusFixed(); - } else if (ArrayLen(local.issues) > local.startCount) { - detailOutput.statusFailed(".env File Configuration"); - } else if (ArrayLen(local.warnings) > local.startWarnings) { - detailOutput.statusWarning(); - } else { - detailOutput.statusSuccess(".env File Configuration"); - } + // Check environment configuration + if(arguments.verbose) { + print.line("Checking environment-specific settings... ").toConsole(); + } + local.startWarnings = ArrayLen(local.warnings); + checkEnvironmentSettings(local.config, local.issues, local.warnings, local.env); + if (ArrayLen(local.warnings) > local.startWarnings) { + detailOutput.statusWarning("Environment-Specific Settings"); + } else { + detailOutput.statusSuccess("Environment-Specific Settings"); + } - // Additional checks for production environment - if (local.env == "production") { + // Check .env file if(arguments.verbose) { - print.line("Checking production-specific requirements... ").toConsole(); + print.line("Checking .env file configuration... ").toConsole(); } local.startCount = ArrayLen(local.issues); local.startWarnings = ArrayLen(local.warnings); - checkProductionSettings(local.config, local.issues, local.warnings); - if (ArrayLen(local.issues) > local.startCount) { - detailOutput.statusFailed("Production-Specific Requirements"); + local.startFixed = ArrayLen(local.fixed); + checkEnvFile(local.issues, local.warnings, arguments.fix, local.fixed); + if (ArrayLen(local.fixed) > local.startFixed) { + detailOutput.statusFixed(); + } else if (ArrayLen(local.issues) > local.startCount) { + detailOutput.statusFailed(".env File Configuration"); } else if (ArrayLen(local.warnings) > local.startWarnings) { - detailOutput.statusWarning(); + detailOutput.statusWarning(".env File Configuration"); } else { - detailOutput.statusSuccess("Production-Specific Requirements"); + detailOutput.statusSuccess(".env File Configuration"); } - } - detailOutput.line(); - detailOutput.divider(); + // Additional checks for production environment + if (local.env == "production") { + if(arguments.verbose) { + print.line("Checking production-specific requirements... ").toConsole(); + } + local.startCount = ArrayLen(local.issues); + local.startWarnings = ArrayLen(local.warnings); + checkProductionSettings(local.config, local.issues, local.warnings); + if (ArrayLen(local.issues) > local.startCount) { + detailOutput.statusFailed("Production-Specific Requirements"); + } else if (ArrayLen(local.warnings) > local.startWarnings) { + detailOutput.statusWarning("Production-Specific Requirements"); + } else { + detailOutput.statusSuccess("Production-Specific Requirements"); + } + } - // Display results - displayResults(local.issues, local.warnings, local.fixed, arguments.verbose); + detailOutput.line(); + detailOutput.divider(); + + // Display results + displayResults(local.issues, local.warnings, local.fixed, arguments.verbose); + + // Return appropriate exit code + if (ArrayLen(local.issues)) { + setExitCode(1); + } - // Return appropriate exit code - if (ArrayLen(local.issues)) { + } catch (any e) { + detailOutput.error("#e.message#"); setExitCode(1); } } @@ -443,7 +449,7 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { for (local.issue in arguments.issues) { print.redLine(" - #local.issue.message#").toConsole(); if (arguments.verbose) { - print.yellowLine(" --> Fix: #local.issue.fix#").toConsole(); + detailOutput.output("Fix: #local.issue.fix#", true); } } detailOutput.line(); @@ -451,11 +457,11 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { // Display warnings if (ArrayLen(arguments.warnings)) { - detailOutput.yellowBold("[WARNINGS] (#ArrayLen(arguments.warnings)#):"); + detailOutput.statusWarning("#ArrayLen(arguments.warnings)#"); for (local.warning in arguments.warnings) { - print.yellowLine(" - #local.warning.message#").toConsole(); + detailOutput.output("#local.warning.message#", true); if (arguments.verbose) { - detailOutput.output(" --> Fix: #local.warning.fix#"); + detailOutput.output("Fix: #local.warning.fix#", true); } } detailOutput.line(); @@ -467,8 +473,8 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { local.warningCount = ArrayLen(arguments.warnings); if (local.errorCount == 0 && local.warningCount == 0) { - print.greenBoldLine("[PASSED] Configuration validation successful!").toConsole(); - print.greenLine(" All checks completed successfully.").toConsole(); + detailOutput.statusSuccess("Configuration validation successful!"); + detailOutput.success(" All checks completed successfully."); } else { local.summary = []; if (local.errorCount > 0) { @@ -479,15 +485,15 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { } if (local.errorCount > 0) { - print.redBoldLine("[FAILED] Configuration check failed").toConsole(); + detailOutput.statusFailed("Configuration check failed"); } else { - detailOutput.yellowBold("[WARNING] Configuration check completed with warnings"); + detailOutput.statusWarning("Configuration check completed with warnings"); } detailOutput.output(" Found: #ArrayToList(local.summary, ', ')#"); if (!arguments.verbose && (local.errorCount > 0 || local.warningCount > 0)) { detailOutput.line(); - detailOutput.output(" Tip: Run with --verbose flag for detailed fix suggestions"); + detailOutput.output("Tip: Run with --verbose flag for detailed fix suggestions", true); } } } diff --git a/cli/src/commands/wheels/config/diff.cfc b/cli/src/commands/wheels/config/diff.cfc index 9af6ce8488..64d87d990a 100644 --- a/cli/src/commands/wheels/config/diff.cfc +++ b/cli/src/commands/wheels/config/diff.cfc @@ -30,119 +30,124 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { boolean env = false, boolean settings = false ) { - requireWheelsApp(getCWD()); - // Reconstruct and validate arguments - arguments = reconstructArgs( - argStruct = arguments, - allowedValues = { - format: ["table", "json"] - } - ); - - // Validate environments are different - if (arguments.env1 == arguments.env2) { - detailOutput.error("Cannot compare an environment to itself"); - return; - } + try{ + requireWheelsApp(getCWD()); + // Reconstruct and validate arguments + arguments = reconstructArgs( + argStruct = arguments, + allowedValues = { + format: ["table", "json"] + } + ); - // Determine what to compare - local.compareSettings = true; - local.compareEnv = true; - - // If both flags are provided or neither, compare both - if (arguments.settings && arguments.env) { - local.compareSettings = true; - local.compareEnv = true; - } else if (arguments.settings) { + // Validate environments are different + if (arguments.env1 == arguments.env2) { + detailOutput.error("Cannot compare an environment to itself"); + return; + } + + // Determine what to compare local.compareSettings = true; - local.compareEnv = false; - } else if (arguments.env) { - local.compareSettings = false; local.compareEnv = true; - } - // If neither flag is provided, compare both (default behavior) - - local.allDifferences = {}; - - // Check if both environments exist (either settings OR .env file) - // Get all file paths first - local.configPath = ResolvePath("config"); - local.settingsFile = local.configPath & "/settings.cfm"; - local.env1SettingsFile = local.configPath & "/" & arguments.env1 & "/settings.cfm"; - local.env2SettingsFile = local.configPath & "/" & arguments.env2 & "/settings.cfm"; - local.env1File = ResolvePath(".env.#arguments.env1#"); - local.env2File = ResolvePath(".env.#arguments.env2#"); - local.baseEnvFile = ResolvePath(".env"); - - // Check if environment 1 exists (either settings OR .env) - local.env1SettingsExists = fileExists(local.env1SettingsFile); - local.env1EnvExists = fileExists(local.env1File) || (arguments.env1 == "development" && fileExists(local.baseEnvFile)); - - if (!local.env1SettingsExists && !local.env1EnvExists) { - detailOutput.statusWarning("Environment '#arguments.env1#' not found!"); - detailOutput.output(" Settings file: #local.env1SettingsFile# (not found)", true); - detailOutput.output(" Env file: #local.env1File# (not found)", true); - detailOutput.statusFailed("Environment '#arguments.env1#' does not exist. No settings file or .env file found."); - return; - } + + // If both flags are provided or neither, compare both + if (arguments.settings && arguments.env) { + local.compareSettings = true; + local.compareEnv = true; + } else if (arguments.settings) { + local.compareSettings = true; + local.compareEnv = false; + } else if (arguments.env) { + local.compareSettings = false; + local.compareEnv = true; + } + // If neither flag is provided, compare both (default behavior) + + local.allDifferences = {}; + + // Check if both environments exist (either settings OR .env file) + // Get all file paths first + local.configPath = ResolvePath("config"); + local.settingsFile = local.configPath & "/settings.cfm"; + local.env1SettingsFile = local.configPath & "/" & arguments.env1 & "/settings.cfm"; + local.env2SettingsFile = local.configPath & "/" & arguments.env2 & "/settings.cfm"; + local.env1File = ResolvePath(".env.#arguments.env1#"); + local.env2File = ResolvePath(".env.#arguments.env2#"); + local.baseEnvFile = ResolvePath(".env"); + + // Check if environment 1 exists (either settings OR .env) + local.env1SettingsExists = fileExists(local.env1SettingsFile); + local.env1EnvExists = fileExists(local.env1File) || (arguments.env1 == "development" && fileExists(local.baseEnvFile)); + + if (!local.env1SettingsExists && !local.env1EnvExists) { + detailOutput.statusWarning("Environment '#arguments.env1#' not found!"); + detailOutput.output("Settings file: #local.env1SettingsFile# (not found)", true); + detailOutput.output("Env file: #local.env1File# (not found)", true); + detailOutput.statusFailed("Environment '#arguments.env1#' does not exist. No settings file or .env file found."); + return; + } - // Check if environment 2 exists (either settings OR .env) - local.env2SettingsExists = fileExists(local.env2SettingsFile); - local.env2EnvExists = fileExists(local.env2File) || (arguments.env2 == "development" && fileExists(local.baseEnvFile)); + // Check if environment 2 exists (either settings OR .env) + local.env2SettingsExists = fileExists(local.env2SettingsFile); + local.env2EnvExists = fileExists(local.env2File) || (arguments.env2 == "development" && fileExists(local.baseEnvFile)); - if (!local.env2SettingsExists && !local.env2EnvExists) { - detailOutput.statusWarning("Environment '#arguments.env2#' not found!"); - detailOutput.output(" Settings file: #local.env2SettingsFile# (not found)", true); - detailOutput.output(" Env file: #local.env2File# (not found)", true); - detailOutput.statusFailed("Environment '#arguments.env2#' does not exist. No settings file or .env file found."); - return; - } - - // Compare settings if requested - if (local.compareSettings) { - if (!FileExists(local.settingsFile)) { - detailOutput.error("No settings.cfm file found in config directory"); + if (!local.env2SettingsExists && !local.env2EnvExists) { + detailOutput.statusWarning("Environment '#arguments.env2#' not found!"); + detailOutput.output("Settings file: #local.env2SettingsFile# (not found)", true); + detailOutput.output("Env file: #local.env2File# (not found)", true); + detailOutput.statusFailed("Environment '#arguments.env2#' does not exist. No settings file or .env file found."); return; } - // Load configurations for both environments (even if files don't exist, will be empty) - local.config1 = loadConfiguration(local.settingsFile, local.env1SettingsFile); - local.config2 = loadConfiguration(local.settingsFile, local.env2SettingsFile); - - // Compare configurations - local.allDifferences.settings = compareConfigurations(local.config1, local.config2); - } + // Compare settings if requested + if (local.compareSettings) { + if (!FileExists(local.settingsFile)) { + detailOutput.error("No settings.cfm file found in config directory"); + return; + } - // Compare environment variables if requested - if (local.compareEnv) { - local.envVars1 = {}; - local.envVars2 = {}; + // Load configurations for both environments (even if files don't exist, will be empty) + local.config1 = loadConfiguration(local.settingsFile, local.env1SettingsFile); + local.config2 = loadConfiguration(local.settingsFile, local.env2SettingsFile); - // Load environment variables for env1 - if (FileExists(local.env1File)) { - local.envVars1 = loadEnvFile(local.env1File); - } else if (arguments.env1 == "development" && FileExists(local.baseEnvFile)) { - // Fall back to .env for development - local.envVars1 = loadEnvFile(local.baseEnvFile); + // Compare configurations + local.allDifferences.settings = compareConfigurations(local.config1, local.config2); } - // Load environment variables for env2 - if (FileExists(local.env2File)) { - local.envVars2 = loadEnvFile(local.env2File); - } else if (arguments.env2 == "development" && FileExists(local.baseEnvFile)) { - // Fall back to .env for development - local.envVars2 = loadEnvFile(local.baseEnvFile); - } + // Compare environment variables if requested + if (local.compareEnv) { + local.envVars1 = {}; + local.envVars2 = {}; + + // Load environment variables for env1 + if (FileExists(local.env1File)) { + local.envVars1 = loadEnvFile(local.env1File); + } else if (arguments.env1 == "development" && FileExists(local.baseEnvFile)) { + // Fall back to .env for development + local.envVars1 = loadEnvFile(local.baseEnvFile); + } - // Compare environment variables - local.allDifferences.env = compareConfigurations(local.envVars1, local.envVars2); - } + // Load environment variables for env2 + if (FileExists(local.env2File)) { + local.envVars2 = loadEnvFile(local.env2File); + } else if (arguments.env2 == "development" && FileExists(local.baseEnvFile)) { + // Fall back to .env for development + local.envVars2 = loadEnvFile(local.baseEnvFile); + } - // Output results - if (arguments.format == "json") { - outputAsJson(local.allDifferences, arguments.env1, arguments.env2, local.compareSettings, local.compareEnv); - } else { - outputAsTable(local.allDifferences, arguments.env1, arguments.env2, arguments.changesOnly, local.compareSettings, local.compareEnv); + // Compare environment variables + local.allDifferences.env = compareConfigurations(local.envVars1, local.envVars2); + } + + // Output results + if (arguments.format == "json") { + outputAsJson(local.allDifferences, arguments.env1, arguments.env2, local.compareSettings, local.compareEnv); + } else { + outputAsTable(local.allDifferences, arguments.env1, arguments.env2, arguments.changesOnly, local.compareSettings, local.compareEnv); + } + }catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } } @@ -353,7 +358,7 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { detailOutput.getPrint().table( data = local.data, headers = [arguments.type == "env" ? "Variable" : "Setting", arguments.env1, arguments.env2] - ); + ).toConsole(); detailOutput.line(); } @@ -371,7 +376,7 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { detailOutput.getPrint().table( data = local.data, headers = [arguments.type == "env" ? "Variable" : "Setting", "Value"] - ); + ).toConsole(); detailOutput.line(); } @@ -389,7 +394,7 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { detailOutput.getPrint().table( data = local.data, headers = [arguments.type == "env" ? "Variable" : "Setting", "Value"] - ); + ).toConsole(); detailOutput.line(); } @@ -406,7 +411,7 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { detailOutput.getPrint().table( data = local.data, headers = [arguments.type == "env" ? "Variable" : "Setting", "Value"] - ); + ).toConsole(); detailOutput.line(); } @@ -526,7 +531,7 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { }; local.jsonData = SerializeJSON(local.output, false, false); - detailOutput.output(deserializeJSON(local.jsonData)); + print.line(deserializeJSON(local.jsonData)).toConsole(); } private string function maskSensitiveValue(required string key, required any value) { diff --git a/cli/src/commands/wheels/env/list.cfc b/cli/src/commands/wheels/env/list.cfc index 373eec9215..cc21e42218 100644 --- a/cli/src/commands/wheels/env/list.cfc +++ b/cli/src/commands/wheels/env/list.cfc @@ -25,36 +25,39 @@ component extends="../base" { boolean verbose = false, boolean check = false, string filter = "All", - string sort = "name", - boolean help = false + string sort = "name" ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs( - argStruct=arguments, - allowedValues={ - format: ["table", "json", "yaml"], - filter: ["All", "Active", "Inactive"], - sort: ["name", "modified", "size"] + try{ + requireWheelsApp(getCWD()); + arguments = reconstructArgs( + argStruct=arguments, + allowedValues={ + format: ["table", "json", "yaml"], + sort: ["name", "modified", "type"] + } + ); + var projectRoot = resolvePath("."); + arguments.rootPath = projectRoot; + var currentEnv = environmentService.getCurrentEnvironment(projectRoot); + + print.line("Checking for available environments...").toConsole(); + + var environments = environmentService.list(argumentCollection=arguments); + + // Handle different format outputs + if (arguments.format == "json") { + var jsonOutput = formatAsJSON(environments, currentEnv); + print.line(jsonOutput).toConsole(); + } else if (arguments.format == "yaml") { + var yamlOutput = formatAsYAML(environments, currentEnv); + print.line(yamlOutput).toConsole(); + } else { + // Table format using detailOutput functions + formatAsTable(environments, arguments.verbose, currentEnv); } - ); - var projectRoot = resolvePath("."); - arguments.rootPath = projectRoot; - var currentEnv = environmentService.getCurrentEnvironment(projectRoot); - - print.line("Checking for available environments...").toConsole(); - - var environments = environmentService.list(argumentCollection=arguments); - - // Handle different format outputs - if (arguments.format == "json") { - var jsonOutput = formatAsJSON(environments, currentEnv); - print.line(jsonOutput).toConsole(); - } else if (arguments.format == "yaml") { - var yamlOutput = formatAsYAML(environments, currentEnv); - print.line(yamlOutput).toConsole(); - } else { - // Table format using detailOutput functions - formatAsTable(environments, arguments.verbose, currentEnv); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } } diff --git a/cli/src/models/EnvironmentService.cfc b/cli/src/models/EnvironmentService.cfc index 45a8dc4aa1..9a8b98f6e0 100644 --- a/cli/src/models/EnvironmentService.cfc +++ b/cli/src/models/EnvironmentService.cfc @@ -140,11 +140,6 @@ component { string sort = "name", boolean help = false ) { - // Show help information if requested - if (arguments.help) { - return getHelpInformation(); - } - var environments = []; var projectRoot = arguments.rootPath; @@ -1517,6 +1512,9 @@ sudo -u postgres psql -c ""GRANT ALL PRIVILEGES ON DATABASE #arguments.databaseN case "production": include = (env.TYPE == "Production"); break; + case "custom": + include = (env.TYPE == "Custom"); + break; case "qa": include = (env.TYPE == "QA"); break; @@ -1655,43 +1653,6 @@ sudo -u postgres psql -c ""GRANT ALL PRIVILEGES ON DATABASE #arguments.databaseN }; } - /** - * Get help information - */ - private function getHelpInformation() { - var help = []; - arrayAppend(help, "wheels env list - List available environments"); - arrayAppend(help, ""); - arrayAppend(help, "Options:"); - arrayAppend(help, " --format Output format (table, json, yaml) [default: table]"); - arrayAppend(help, " --verbose Show detailed configuration"); - arrayAppend(help, " --check Validate environment configurations"); - arrayAppend(help, " --filter Filter by environment type"); - arrayAppend(help, " --sort Sort by (name, type, modified) [default: name]"); - arrayAppend(help, " --help Show this help information"); - arrayAppend(help, ""); - arrayAppend(help, "Filter options:"); - arrayAppend(help, " All Show all environments (default)"); - arrayAppend(help, " local Local environments only"); - arrayAppend(help, " development Development environments"); - arrayAppend(help, " staging Staging environments"); - arrayAppend(help, " production Production environments"); - arrayAppend(help, " file File-based environments"); - arrayAppend(help, " server.json Server.json environments"); - arrayAppend(help, " valid Valid environments only"); - arrayAppend(help, " issues Environments with issues"); - arrayAppend(help, ""); - arrayAppend(help, "Examples:"); - arrayAppend(help, " wheels env list"); - arrayAppend(help, " wheels env list --verbose"); - arrayAppend(help, " wheels env list --format json"); - arrayAppend(help, " wheels env list --filter production --check"); - arrayAppend(help, " wheels env list --sort modified --verbose"); - - return arrayToList(help, chr(10)); - } - - /** * Gets the current environment using the same logic as Application.cfc * @projectRoot The root directory of the CFWheels project diff --git a/docs/src/command-line-tools/commands/environment/env-list.md b/docs/src/command-line-tools/commands/environment/env-list.md index 09904d4055..723f90935b 100644 --- a/docs/src/command-line-tools/commands/environment/env-list.md +++ b/docs/src/command-line-tools/commands/environment/env-list.md @@ -21,7 +21,6 @@ The `wheels env list` command displays all configured environments in your Wheel | `--check` | Validate environment configurations | `false` | | `--filter` | Filter by environment type | All | | `--sort` | Sort by (name, type, modified) | `name` | -| `--help` | Show help information | ## Examples From 0c81110306edc04d77173ee722d76049fde057f5 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 19 Jan 2026 17:05:46 +0500 Subject: [PATCH 012/405] commit: fixes for cli issues --- cli/src/commands/wheels/generate/view.cfc | 24 ++++++++++++----------- cli/src/models/AnalysisService.cfc | 3 ++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cli/src/commands/wheels/generate/view.cfc b/cli/src/commands/wheels/generate/view.cfc index 9f6c0ff3c0..3011c61acb 100644 --- a/cli/src/commands/wheels/generate/view.cfc +++ b/cli/src/commands/wheels/generate/view.cfc @@ -40,8 +40,10 @@ component aliases='wheels g view' extends="../base" { ); var obj = helpers.getNameVariants(listLast( arguments.objectName, '/\' )); var viewdirectory = fileSystemUtil.resolvePath( "app/views" ); + var viewFolderName = lcase(listLast( arguments.objectName, '/\' )); // Build path from resolved viewdirectory to avoid conflicts with existing directories (e.g., tests/) - var directory = viewdirectory & "/" & obj.objectNamePlural; + // Use the provided objectName directly instead of forcing plural + var directory = viewdirectory & "/" & viewFolderName; // Handle multiple views if comma-separated list is provided var viewNames = listToArray(arguments.name); @@ -57,11 +59,11 @@ component aliases='wheels g view' extends="../base" { return; } - // Validate views subdirectory, create if doesnt' exist - if( !directoryExists( directory ) ) { - directoryCreate(directory); - detailOutput.create("app/views/" & obj.objectNamePlural); - } + // Validate views subdirectory, create if doesnt' exist + if( !directoryExists( directory ) ) { + directoryCreate(directory); + detailOutput.create("app/views/" & viewFolderName); + } //Copy template files to the application folder if they do not exist there ensureSnippetTemplatesExist(); @@ -104,13 +106,13 @@ component aliases='wheels g view' extends="../base" { if(fileExists(viewPath)){ if(arguments.force || confirm( '#viewName# already exists in target directory. Do you want to overwrite? [y/n]' ) ) { - detailOutput.update("app/views/" & obj.objectNamePlural & "/" & viewName); + detailOutput.update("app/views/" & viewFolderName & "/" & viewName); } else { - detailOutput.skip("app/views/" & obj.objectNamePlural & "/" & viewName); + detailOutput.skip("app/views/" & viewFolderName & "/" & viewName); continue; } } else { - detailOutput.create("app/views/" & obj.objectNamePlural & "/" & viewName); + detailOutput.create("app/views/" & viewFolderName & "/" & viewName); } file action='write' file='#viewPath#' mode ='777' output='#trim( viewContent )#'; arrayAppend(generatedViews, viewName); @@ -121,9 +123,9 @@ component aliases='wheels g view' extends="../base" { var nextSteps = []; if (arrayLen(generatedViews) == 1) { - arrayAppend(nextSteps, "Review the generated view at app/views/" & obj.objectNamePlural & "/" & generatedViews[1]); + arrayAppend(nextSteps, "Review the generated view at app/views/" & viewFolderName & "/" & generatedViews[1]); } else { - arrayAppend(nextSteps, "Review the generated views in app/views/" & obj.objectNamePlural & "/"); + arrayAppend(nextSteps, "Review the generated views in app/views/" & viewFolderName & "/"); } arrayAppend(nextSteps, "Customize the HTML content as needed"); diff --git a/cli/src/models/AnalysisService.cfc b/cli/src/models/AnalysisService.cfc index 056af4ec94..f62d44da10 100644 --- a/cli/src/models/AnalysisService.cfc +++ b/cli/src/models/AnalysisService.cfc @@ -146,7 +146,8 @@ component { "packages/", "coldbox/", "modules/", - "WEB-INF/" + "WEB-INF/", + "migrator/" ], "wheels": { "check-deprecated": true, From b39abdd899372e8e7271022c4efe8200c9982df2 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Tue, 20 Jan 2026 15:27:07 +0500 Subject: [PATCH 013/405] commit: fixed wheels cli command error instructions Updated wheels CLI command failed instructions - Updated error instructions for wheels db create command if djbc drivers are not installed by default for oracle and sqlite databases --- cli/src/commands/wheels/db/create.cfc | 91 ++++++++++++++++++- .../commands/database/db-create.md | 42 ++++++--- 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/cli/src/commands/wheels/db/create.cfc b/cli/src/commands/wheels/db/create.cfc index 6bc4f38a22..4276e4f13a 100644 --- a/cli/src/commands/wheels/db/create.cfc +++ b/cli/src/commands/wheels/db/create.cfc @@ -238,6 +238,44 @@ component extends="../base" { if (!local.driverFound) { detailOutput.error("No " & arguments.dbType & " driver found. Ensure JDBC driver is in classpath."); + // Provide database-specific guidance for missing drivers + switch (arguments.dbType) { + case "Oracle": + local.cbHome = getCommandBoxHome(); + local.libPath = formatLibPath(local.cbHome); + detailOutput.divider(); + detailOutput.statusWarning("Oracle JDBC Driver Installation:"); + detailOutput.line(); + detailOutput.output("1. Download the driver from Oracle's official website:"); + detailOutput.output(" https://www.oracle.com/database/technologies/appdev/jdbc-downloads.html"); + detailOutput.output(" - Download 'ojdbc11.jar' or 'ojdbc8.jar'"); + detailOutput.line(); + + if (len(local.cbHome)) { + detailOutput.output("2. Place the JAR file in this directory:"); + detailOutput.output(" #local.libPath#"); + } else { + detailOutput.output("2. Place the JAR file in CommandBox's library directory:"); + detailOutput.output(" #local.libPath#"); + } + + detailOutput.line(); + detailOutput.output("3. Restart CommandBox completely:"); + detailOutput.output(" - Close all CommandBox instances (don't just reload)"); + detailOutput.output(" - This ensures the JDBC driver is properly loaded"); + detailOutput.line(); + detailOutput.output("4. Verify installation:"); + detailOutput.output(" wheels db create datasource=#arguments.dsInfo.datasource#"); + detailOutput.output(" You should see: '[OK] Driver found: oracle.jdbc.OracleDriver'"); + break; + + default: + detailOutput.statusWarning("Driver installation guidance:"); + detailOutput.output("1. Restart CommandBox completely"); + detailOutput.output("2. Check CommandBox lib directory for appropriate JAR files"); + detailOutput.output("3. Ensure required bundles are installed"); + } + return; } @@ -1206,8 +1244,14 @@ component extends="../base" { } else if (FindNoCase("driver not found", local.errorMessage) || FindNoCase("JDBC driver", local.errorMessage)) { detailOutput.statusFailed("SQLite JDBC driver not found"); - detailOutput.statusWarning("Ensure org.xerial.sqlite-jdbc is in the classpath"); - detailOutput.statusWarning("You may need to add the driver to your application server"); + detailOutput.statusWarning("SQLite requires the org.xerial.sqlite-jdbc driver in the classpath"); + detailOutput.line(); + detailOutput.output("SQLite JDBC Driver Installation:"); + detailOutput.output("1. SQLite driver should be included with CommandBox/Lucee"); + detailOutput.output("2. If missing, ensure org.xerial.sqlite-jdbc bundle is installed"); + detailOutput.output("3. Try restarting CommandBox completely"); + detailOutput.output("4. Check that the driver is in the CommandBox lib directory"); + detailOutput.line(); local.errorHandled = true; } else if (FindNoCase("Failed to create", local.errorMessage)) { detailOutput.statusFailed("Failed to create SQLite database connection"); @@ -1238,6 +1282,49 @@ component extends="../base" { throw(message=arguments.e.message, detail=(StructKeyExists(arguments.e, "detail") ? arguments.e.detail : "")); } + /** + * Get CommandBox Home directory by running info --json command + * Returns CLIHome path or empty string if not found + */ + private string function getCommandBoxHome() { + local.infoResult = command("info").params(json=true).run(returnOutput=true); + local.cleanResult = reReplace( + local.infoResult, + "\x1B\[[0-9;]*[A-Za-z]", + "", + "all" + ); + + local.cleanResult = trim( local.cleanResult ); + + // Parse JSON response to extract CLIHome + if (isJSON(local.cleanResult)) { + local.infoData = deserializeJSON(local.cleanResult); + if (structKeyExists(local.infoData, "CLIHome") && len(local.infoData.CLIHome)) { + return local.infoData.CLIHome; + } + } + + return ""; + } + + /** + * Format file path with proper separators for the user's OS + */ + private string function formatLibPath(required string homeDir) { + if (!len(arguments.homeDir)) { + return "path/to/CommandBox/lib"; + } + + // For Mac/Linux, use forward slashes + if (server.os.name contains "mac" || server.os.name contains "linux" || server.os.name contains "unix") { + return arguments.homeDir & "/lib"; + } + + // For Windows, use backslashes + return arguments.homeDir & "\lib"; + } + /** * Normalize database type to CLI parameter format * Converts internal database type names to lowercase short form expected by env setup diff --git a/docs/src/command-line-tools/commands/database/db-create.md b/docs/src/command-line-tools/commands/database/db-create.md index 5a05f5ec0a..a577b92561 100644 --- a/docs/src/command-line-tools/commands/database/db-create.md +++ b/docs/src/command-line-tools/commands/database/db-create.md @@ -18,6 +18,7 @@ The `wheels db create` command creates a new database using the connection infor - **Interactive datasource creation**: Prompts for credentials when datasource doesn't exist - **Environment validation**: Checks if environment exists before prompting for credentials - **Smart error handling**: Single, clear error messages without duplication +- **Enhanced driver guidance**: Provides specific installation instructions when JDBC drivers are missing - **Post-creation setup**: Automatically creates environment files and writes datasource to `app.cfm` after successful database creation ## Options @@ -293,21 +294,28 @@ wheels db create --dbtype=oracle --database=myapp_test # ✓ Correct If you see "Driver not found" error, you need to manually install the Oracle JDBC driver: 1. **Download the driver** from [Oracle's official website](https://www.oracle.com/database/technologies/appdev/jdbc-downloads.html) - - Download `ojdbc11.jar` or `ojdbc8.jar` + - Download `ojdbc11.jar` or `ojdbc8.jar` -2. **Place the JAR file** in CommandBox's JRE library directory: - - **Windows**: `path\to\CommandBox\jre\lib\` - - **Mac/Linux**: `/usr/local/lib/CommandBox/jre/lib/` +2. **Find the correct location on *your* machine** + - Run this in CommandBox: + ```bash + info + ``` + - Look for the line: **CommandBox Home** `/Users/yourname/.CommandBox`, there you will be able to find the exact commandBox path. -3. **Restart CommandBox completely**: - - **Important**: Close ALL CommandBox instances (don't just reload) - - This ensures the JDBC driver is properly loaded +3. **Place the JAR file** in CommandBox's library directory: + - **Windows**: `path\to\CommandBox\lib\` + - **Mac/Linux**: `path\to\CommandBox/lib/` -4. **Verify installation**: - ```bash - wheels db create datasource=myOracleDS - ``` - You should see: `[OK] Driver found: oracle.jdbc.OracleDriver` +4. **Restart CommandBox completely**: + - **Important**: Close all CommandBox instances (don't just reload) + - This ensures the JDBC driver is properly loaded + +5. **Verify installation**: + ```bash + wheels db create datasource=myOracleDS + ``` + You should see: `[OK] Driver found: oracle.jdbc.OracleDriver` **Common Oracle Errors:** - **"Invalid Oracle identifier"**: Database name contains hyphens. Use underscores instead. @@ -377,9 +385,15 @@ No datasource was specified and none could be found in your Wheels configuration The specified datasource doesn't exist in your server configuration. The command will prompt you to create it interactively. ### "Driver not found" (Oracle-specific) -Oracle JDBC driver is not installed in CommandBox. +The JDBC driver for the database type is not available in CommandBox by default. + +**Fix:** The CLI will automatically provide specific installation guidance when a driver is missing: + +- **Oracle:** Shows detailed step-by-step Oracle JDBC driver installation instructions +- **MySQL/PostgreSQL/MSSQL/SQLite:** Provides guidance on verifying CommandBox installation +- **General:** Suggests restarting CommandBox and checking library directory -**Fix:** See the [Oracle Database](#oracle-database) section above for detailed installation instructions. +**Fix:** See the [Oracle Database](#oracle-database) section above for detailed instructions. ### "Database already exists" The database already exists. Use `--force` flag to drop and recreate it: From adcdc91d398fb9df09ea04ee7d715d0ad7d38e6a Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Tue, 20 Jan 2026 17:57:38 +0500 Subject: [PATCH 014/405] commit: added option for service name for oracle database creation --- cli/src/commands/wheels/base.cfc | 7 +- cli/src/commands/wheels/db/create.cfc | 103 ++++++++++++++++++++------ cli/src/commands/wheels/env/setup.cfc | 37 +++++++-- cli/src/models/EnvironmentService.cfc | 88 +++++++++++++++++++--- 4 files changed, 195 insertions(+), 40 deletions(-) diff --git a/cli/src/commands/wheels/base.cfc b/cli/src/commands/wheels/base.cfc index e26898b8fe..80b1601f5d 100644 --- a/cli/src/commands/wheels/base.cfc +++ b/cli/src/commands/wheels/base.cfc @@ -1011,7 +1011,12 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { if (!Len(local.port)) local.port = "1521"; // Connect using SID (Oracle system identifier) local.sid = arguments.dsInfo.sid ?: "FREE"; - return "jdbc:oracle:thin:@#local.host#:#local.port#:#local.sid#"; + // Build Oracle JDBC URL + if (StructKeyExists(arguments.dsInfo, "serviceName") && Len(arguments.dsInfo.serviceName)) { + return "jdbc:oracle:thin:@//#local.host#:#local.port#/#arguments.dsInfo.serviceName#"; + } else { + return "jdbc:oracle:thin:@#local.host#:#local.port#:#local.sid#"; + } case "H2": // H2 databases are created automatically, no system database needed local.database = arguments.dsInfo.database ?: ""; diff --git a/cli/src/commands/wheels/db/create.cfc b/cli/src/commands/wheels/db/create.cfc index 4276e4f13a..e232f28738 100644 --- a/cli/src/commands/wheels/db/create.cfc +++ b/cli/src/commands/wheels/db/create.cfc @@ -265,7 +265,7 @@ component extends="../base" { detailOutput.output(" - This ensures the JDBC driver is properly loaded"); detailOutput.line(); detailOutput.output("4. Verify installation:"); - detailOutput.output(" wheels db create datasource=#arguments.dsInfo.datasource#"); + detailOutput.output(" wheels db create datasource=YourDataSourceName"); detailOutput.output(" You should see: '[OK] Driver found: oracle.jdbc.OracleDriver'"); break; @@ -748,17 +748,46 @@ component extends="../base" { local.password = ask("Password: ", "", true); // true for password masking - // For Oracle, ask for SID + // For Oracle, ask for connection type and details if (local.dbType == "Oracle") { - local.sid = ask("SID [FREE]: "); - if (!len(local.sid)) { - local.sid = "FREE"; + detailOutput.output("Oracle Connection Type:"); + detailOutput.output("1. SID (System Identifier)"); + detailOutput.output("2. Service Name"); + detailOutput.line(); + + local.connectionTypeChoice = ask("Select connection type [1-2]: "); + + if (local.connectionTypeChoice == "2") { + // Service Name + local.serviceName = ask("Service Name: "); + if (!len(local.serviceName)) { + detailOutput.statusWarning("Service Name is required"); + return {}; + } + local.oracleConnectionType = "servicename"; + local.oracleIdentifier = local.serviceName; + } else { + // SID (default) + local.sid = ask("SID [FREE]: "); + if (!len(local.sid)) { + local.sid = "FREE"; + } + local.oracleConnectionType = "sid"; + local.oracleIdentifier = local.sid; } } } // Build connection string - local.connectionString = buildConnectionString(local.dbType, local.host, local.port, local.database, local.sid ?: ""); + local.connectionString = buildConnectionString( + local.dbType, + local.host, + local.port, + local.database, + local.sid ?: "", + local.serviceName ?: "", + local.oracleConnectionType ?: "sid" + ); detailOutput.subHeader("Configuration Review", 50); detailOutput.metric("Datasource Name", arguments.datasourceName); @@ -771,6 +800,14 @@ component extends="../base" { detailOutput.metric("Database", local.database); + if (local.dbType == "Oracle") { + if (local.oracleConnectionType == "servicename") { + detailOutput.metric("Service Name", local.serviceName); + } else { + detailOutput.metric("SID", local.sid); + } + } + if (local.dbType != "SQLite") { detailOutput.metric("Username", local.username); } @@ -810,6 +847,8 @@ component extends="../base" { username = local.username, password = local.password, sid = local.sid ?: "", + servicename = local.serviceName ?: "", + oracleConnectionType = local.oracleConnectionType ?: "sid", skipDatabase = true ) .run(); @@ -822,15 +861,27 @@ component extends="../base" { } // Return datasource info in the format expected by the rest of the code - return { + local.result = { driver: local.dbType, database: local.database, host: local.host, port: local.port, username: local.username, - password: local.password, - sid: local.sid ?: "" + password: local.password }; + + // Add Oracle-specific connection information + if (local.dbType == "Oracle") { + if (local.oracleConnectionType == "servicename") { + local.result.servicename = local.serviceName; + local.result.oracleConnectionType = "servicename"; + } else { + local.result.sid = local.sid; + local.result.oracleConnectionType = "sid"; + } + } + + return local.result; } /** @@ -911,7 +962,7 @@ component extends="../base" { /** * Build connection string */ - private string function buildConnectionString(required string dbType, required string host, required string port, required string database, string sid = "") { + private string function buildConnectionString(required string dbType, required string host, required string port, required string database, string sid = "", string servicename = "", string oracleConnectionType = "sid") { switch (arguments.dbType) { case "MySQL": return "jdbc:mysql://#arguments.host#:#arguments.port#/#arguments.database#?characterEncoding=UTF-8&serverTimezone=UTC&maxReconnects=3"; @@ -922,7 +973,11 @@ component extends="../base" { case "MSSQLServer": return "jdbc:sqlserver://#arguments.host#:#arguments.port#;DATABASENAME=#arguments.database#;trustServerCertificate=true;SelectMethod=direct"; case "Oracle": - return "jdbc:oracle:thin:@#arguments.host#:#arguments.port#:#arguments.sid#"; + if (arguments.oracleConnectionType == "servicename") { + return "jdbc:oracle:thin:@#arguments.host#:#arguments.port#/#arguments.servicename#"; + } else { + return "jdbc:oracle:thin:@#arguments.host#:#arguments.port#:#arguments.sid#"; + } case "H2": local.appPath = getCWD(); return "jdbc:h2:#local.appPath#db/h2/#arguments.database#;MODE=MySQL"; @@ -1010,18 +1065,20 @@ component extends="../base" { // Call wheels env setup with skipDatabase to avoid infinite loop command("wheels env setup") - .params( - environment = arguments.environment, - dbtype = normalizeDbType(arguments.dbType), - datasource = arguments.datasource, - database = arguments.dsInfo.database, - host = arguments.dsInfo.host ?: "localhost", - port = arguments.dsInfo.port ?: "", - username = arguments.dsInfo.username ?: "root", - password = arguments.dsInfo.password ?: "", - sid = arguments.dsInfo.sid ?: "", - skipDatabase = true - ) + .params( + environment = arguments.environment, + dbtype = normalizeDbType(arguments.dbType), + datasource = arguments.datasource, + database = arguments.dsInfo.database, + host = arguments.dsInfo.host ?: "localhost", + port = arguments.dsInfo.port ?: "", + username = arguments.dsInfo.username ?: "root", + password = arguments.dsInfo.password ?: "", + sid = arguments.dsInfo.sid ?: "", + servicename = arguments.dsInfo.servicename ?: "", + oracleConnectionType = arguments.dsInfo.oracleConnectionType ?: "sid", + skipDatabase = true + ) .run(); detailOutput.statusSuccess("Environment configuration created!"); diff --git a/cli/src/commands/wheels/env/setup.cfc b/cli/src/commands/wheels/env/setup.cfc index 7f5bc03a7d..93017e78cd 100644 --- a/cli/src/commands/wheels/env/setup.cfc +++ b/cli/src/commands/wheels/env/setup.cfc @@ -30,7 +30,9 @@ component extends="../base" { string port = "", string username = "", string password = "", - string sid = "", + string sid = "", + string servicename = "", + string oracleConnectionType = "sid", boolean force = false, string base = "", boolean debug = false, @@ -157,11 +159,34 @@ component extends="../base" { arguments.password = ask(message="Database Password: ", mask="*"); } - // Prompt for SID if Oracle - if (lCase(arguments.dbtype) == "oracle" && !len(trim(arguments.sid))) { - arguments.sid = ask("SID [FREE]: "); - if (!len(trim(arguments.sid))) { - arguments.sid = "FREE"; + // Prompt for Oracle connection details if Oracle + if (lCase(arguments.dbtype) == "oracle") { + if (!len(trim(arguments.oracleConnectionType)) || + (!len(trim(arguments.sid)) && !len(trim(arguments.servicename)))) { + + detailOutput.output("Oracle Connection Type:"); + detailOutput.output("1. SID (System Identifier)"); + detailOutput.output("2. Service Name"); + detailOutput.line(); + + var connectionTypeChoice = ask("Select connection type [1-2]: "); + + if (connectionTypeChoice == "2") { + // Service Name + arguments.servicename = ask("Service Name: "); + if (!len(trim(arguments.servicename))) { + detailOutput.statusWarning("Service Name is required"); + return; + } + arguments.oracleConnectionType = "servicename"; + } else { + // SID (default) + arguments.sid = ask("SID [FREE]: "); + if (!len(trim(arguments.sid))) { + arguments.sid = "FREE"; + } + arguments.oracleConnectionType = "sid"; + } } } diff --git a/cli/src/models/EnvironmentService.cfc b/cli/src/models/EnvironmentService.cfc index 9a8b98f6e0..3618bb320b 100644 --- a/cli/src/models/EnvironmentService.cfc +++ b/cli/src/models/EnvironmentService.cfc @@ -19,6 +19,8 @@ component { string username = "", string password = "", string sid = "", + string servicename = "", + string oracleConnectionType = "sid", boolean force = false, string base = "", boolean debug = false, @@ -49,7 +51,9 @@ component { port = arguments.port, username = arguments.username, password = arguments.password, - sid = arguments.sid + sid = arguments.sid, + servicename = arguments.servicename, + oracleConnectionType = arguments.oracleConnectionType ); } @@ -493,6 +497,8 @@ component { var dbUsername = len(trim(arguments.username)) ? arguments.username : getDefaultUsername(arguments.dbtype); var dbPassword = len(trim(arguments.password)) ? arguments.password : getDefaultPassword(arguments.dbtype); var dbSid = len(trim(arguments.sid)) ? arguments.sid : "ORCL"; + var dbServiceName = len(trim(arguments.servicename)) ? arguments.servicename : ""; + var dbOracleConnectionType = len(trim(arguments.oracleConnectionType)) ? arguments.oracleConnectionType : "sid"; // Database-specific configuration switch (arguments.dbtype) { @@ -530,7 +536,7 @@ component { }; break; case "oracle": - config.datasourceInfo = { + var oracleDsInfo = { driver: "Oracle", host: dbHost, port: dbPort, @@ -538,8 +544,17 @@ component { datasource: datasourceName, username: dbUsername, password: dbPassword, - sid: dbSid + oracleConnectionType: dbOracleConnectionType }; + + // Add SID or Service Name based on connection type + if (dbOracleConnectionType == "servicename") { + oracleDsInfo.servicename = dbServiceName; + } else { + oracleDsInfo.sid = dbSid; + } + + config.datasourceInfo = oracleDsInfo; break; case "sqlite": // SQLite requires absolute path - calculate it now @@ -751,9 +766,17 @@ component { arrayAppend(envContent, "DB_USER=#arguments.config.datasourceInfo.username#"); arrayAppend(envContent, "DB_PASSWORD=#arguments.config.datasourceInfo.password#"); - // Add Oracle SID if exists - if (arguments.config.dbtype == "oracle" && structKeyExists(arguments.config.datasourceInfo, "sid")) { - arrayAppend(envContent, "DB_SID=#arguments.config.datasourceInfo.sid#"); + // Add Oracle connection details if exists + if (arguments.config.dbtype == "oracle") { + if (structKeyExists(arguments.config.datasourceInfo, "sid") && len(trim(arguments.config.datasourceInfo.sid))) { + arrayAppend(envContent, "DB_SID=#arguments.config.datasourceInfo.sid#"); + } + if (structKeyExists(arguments.config.datasourceInfo, "servicename") && len(trim(arguments.config.datasourceInfo.servicename))) { + arrayAppend(envContent, "DB_SERVICENAME=#arguments.config.datasourceInfo.servicename#"); + } + if (structKeyExists(arguments.config.datasourceInfo, "oracleConnectionType")) { + arrayAppend(envContent, "DB_ORACLE_CONNECTION_TYPE=#arguments.config.datasourceInfo.oracleConnectionType#"); + } } // Add datasource name @@ -970,10 +993,14 @@ component { }; break; case "oracle": + // Oracle connection string depends on connection type (SID vs Service Name) + // Check for Service Name first, fallback to SID config = { class: "oracle.jdbc.OracleDriver", bundleName: "org.lucee.oracle", - connectionString: "jdbc:oracle:thin:@##this.env.DB_HOST##:##this.env.DB_PORT##:##this.env.DB_SID##" + connectionString: "jdbc:oracle:thin:@##this.env.DB_HOST##:##this.env.DB_PORT##" & + "##(structKeyExists(this.env, 'DB_ORACLE_CONNECTION_TYPE') && this.env.DB_ORACLE_CONNECTION_TYPE == 'servicename' ? '/' : ':')##" & + "##(structKeyExists(this.env, 'DB_ORACLE_CONNECTION_TYPE') && this.env.DB_ORACLE_CONNECTION_TYPE == 'servicename' ? this.env.DB_SERVICENAME : this.env.DB_SID)##" }; break; case "sqlite": @@ -1030,6 +1057,17 @@ component { serverJson.env[arguments.environment]["DB_DATABASE"] = ds.database; serverJson.env[arguments.environment]["DB_USER"] = ds.username; serverJson.env[arguments.environment]["DB_PASSWORD"] = ds.password; + + // Add Oracle-specific connection details + if (structKeyExists(ds, "oracleConnectionType")) { + serverJson.env[arguments.environment]["DB_ORACLE_CONNECTION_TYPE"] = ds.oracleConnectionType; + } + if (structKeyExists(ds, "sid") && len(trim(ds.sid))) { + serverJson.env[arguments.environment]["DB_SID"] = ds.sid; + } + if (structKeyExists(ds, "servicename") && len(trim(ds.servicename))) { + serverJson.env[arguments.environment]["DB_SERVICENAME"] = ds.servicename; + } } fileWrite(serverJsonPath, serializeJSON(serverJson, true)); @@ -1728,7 +1766,9 @@ sudo -u postgres psql -c ""GRANT ALL PRIVILEGES ON DATABASE #arguments.databaseN required string port, required string username, required string password, - required string sid + required string sid, + string servicename = "", + string oracleConnectionType = "sid" ) { try { // Read existing environment file @@ -1743,6 +1783,8 @@ sudo -u postgres psql -c ""GRANT ALL PRIVILEGES ON DATABASE #arguments.databaseN DB_USER: false, DB_PASSWORD: false, DB_SID: false, + DB_SERVICENAME: false, + DB_ORACLE_CONNECTION_TYPE: false, DB_DATASOURCE: false }; var inDatabaseSection = false; @@ -1816,12 +1858,30 @@ sudo -u postgres psql -c ""GRANT ALL PRIVILEGES ON DATABASE #arguments.databaseN // Update DB_SID (Oracle only) if (findNoCase("DB_SID=", trimmedLine) == 1) { dbVarsFound.DB_SID = true; - if (lCase(arguments.dbtype) == "oracle" && len(trim(arguments.sid))) { + if (lCase(arguments.dbtype) == "oracle" && len(trim(arguments.sid)) && arguments.oracleConnectionType == "sid") { arrayAppend(updatedLines, "DB_SID=#arguments.sid#"); } continue; } + // Update DB_SERVICENAME (Oracle Service Name only) + if (findNoCase("DB_SERVICENAME=", trimmedLine) == 1) { + dbVarsFound.DB_SERVICENAME = true; + if (lCase(arguments.dbtype) == "oracle" && len(trim(arguments.servicename)) && arguments.oracleConnectionType == "servicename") { + arrayAppend(updatedLines, "DB_SERVICENAME=#arguments.servicename#"); + } + continue; + } + + // Update DB_ORACLE_CONNECTION_TYPE (Oracle only) + if (findNoCase("DB_ORACLE_CONNECTION_TYPE=", trimmedLine) == 1) { + dbVarsFound.DB_ORACLE_CONNECTION_TYPE = true; + if (lCase(arguments.dbtype) == "oracle" && len(trim(arguments.oracleConnectionType))) { + arrayAppend(updatedLines, "DB_ORACLE_CONNECTION_TYPE=#arguments.oracleConnectionType#"); + } + continue; + } + // Update DB_DATASOURCE if (findNoCase("DB_DATASOURCE=", trimmedLine) == 1) { dbVarsFound.DB_DATASOURCE = true; @@ -1862,10 +1922,18 @@ sudo -u postgres psql -c ""GRANT ALL PRIVILEGES ON DATABASE #arguments.databaseN arrayAppend(missingVars, "DB_PASSWORD=#arguments.password#"); } - if (!dbVarsFound.DB_SID && lCase(arguments.dbtype) == "oracle" && len(trim(arguments.sid))) { + if (!dbVarsFound.DB_SID && lCase(arguments.dbtype) == "oracle" && len(trim(arguments.sid)) && arguments.oracleConnectionType == "sid") { arrayAppend(missingVars, "DB_SID=#arguments.sid#"); } + if (!dbVarsFound.DB_SERVICENAME && lCase(arguments.dbtype) == "oracle" && len(trim(arguments.servicename)) && arguments.oracleConnectionType == "servicename") { + arrayAppend(missingVars, "DB_SERVICENAME=#arguments.servicename#"); + } + + if (!dbVarsFound.DB_ORACLE_CONNECTION_TYPE && lCase(arguments.dbtype) == "oracle" && len(trim(arguments.oracleConnectionType))) { + arrayAppend(missingVars, "DB_ORACLE_CONNECTION_TYPE=#arguments.oracleConnectionType#"); + } + if (!dbVarsFound.DB_DATASOURCE) { var dsName = len(trim(arguments.datasource)) ? arguments.datasource : "wheels_#arguments.environment#"; arrayAppend(missingVars, "DB_DATASOURCE=#dsName#"); From 3e5f626427d91679106727cd6373e7753dfaf237 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 21 Jan 2026 16:26:57 +0500 Subject: [PATCH 015/405] commit: added check for oracle admin privileges for db creation --- cli/src/commands/wheels/db/create.cfc | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/cli/src/commands/wheels/db/create.cfc b/cli/src/commands/wheels/db/create.cfc index e232f28738..8bb873c150 100644 --- a/cli/src/commands/wheels/db/create.cfc +++ b/cli/src/commands/wheels/db/create.cfc @@ -309,6 +309,21 @@ component extends="../base" { } detailOutput.statusSuccess("Connected successfully to #arguments.dbType# server!"); + // For Oracle, check admin privileges before proceeding + if (arguments.dbType == "Oracle") { + detailOutput.output("Checking Oracle user privileges..."); + local.privilegeCheck = checkOraclePrivileges(local.conn, local.username); + + if (!local.privilegeCheck.hasRequiredPrivileges) { + detailOutput.statusFailed("Oracle user '#local.username#' does not have sufficient admin privileges."); + detailOutput.line(); + detailOutput.statusWarning("Please provide credentials for an Oracle user with required privileges"); + local.conn.close(); + return; + } + + detailOutput.statusSuccess("Oracle user has required admin privileges."); + } // Check if database already exists print.line("Checking if database exists...").toConsole(); @@ -789,6 +804,36 @@ component extends="../base" { local.oracleConnectionType ?: "sid" ); + // For Oracle, show admin privilege requirement and get user acknowledgment + if (local.dbType == "Oracle") { + detailOutput.subHeader("Oracle Admin Privileges Required", 50); + + detailOutput.output("The provided Oracle user must be an ADMIN user."); + detailOutput.output("This means the user must have at least one system privilege"); + detailOutput.output("granted with ADMIN OPTION = YES."); + detailOutput.line(); + + detailOutput.output("Examples of such users:"); + detailOutput.output(" - SYSTEM"); + detailOutput.output(" - Custom DBA users with admin privileges"); + detailOutput.line(); + + detailOutput.output("Normal application users will NOT work."); + detailOutput.line(); + + local.acknowledgment = ask( + "Do you acknowledge that the provided credentials must be an Oracle admin user? [y/n]: " + ); + + if (local.acknowledgment != "y") { + detailOutput.statusWarning("Datasource creation cancelled"); + return {}; + } + + detailOutput.statusSuccess("Admin privilege requirement acknowledged."); + } + + detailOutput.subHeader("Configuration Review", 50); detailOutput.metric("Datasource Name", arguments.datasourceName); detailOutput.metric("Database Type", local.dbType); @@ -1339,6 +1384,49 @@ component extends="../base" { throw(message=arguments.e.message, detail=(StructKeyExists(arguments.e, "detail") ? arguments.e.detail : "")); } + /** + * Check Oracle user privileges for database creation + * Verifies that the user has CREATE USER and GRANT ANY PRIVILEGE + */ + private struct function checkOraclePrivileges(required any conn, required string username) { + local.result = { + hasRequiredPrivileges = false, + missingPrivileges = [] + }; + + try { + local.stmt = arguments.conn.createStatement(); + + // Query to check user system privileges + local.query = "SELECT privilege, admin_option FROM user_sys_privs"; + local.rs = local.stmt.executeQuery(local.query); + + local.privilegesWithAdmin = []; + while (local.rs.next()) { + if (ucase(local.rs.getString("admin_option")) == "YES") { + arrayAppend(local.privilegesWithAdmin, ucase(local.rs.getString("privilege"))); + } + } + + local.rs.close(); + local.stmt.close(); + + // If user has any privileges with ADMIN_OPTION = YES, consider admin + if (!arrayIsEmpty(local.privilegesWithAdmin)) { + local.result.hasRequiredPrivileges = true; + } else { + local.result.hasRequiredPrivileges = false; + arrayAppend(local.result.missingPrivileges, "No privileges with ADMIN_OPTION = YES found"); + } + + } catch (any e) { + local.result.hasRequiredPrivileges = false; + arrayAppend(local.result.missingPrivileges, "Unable to verify privileges - may lack access to user_sys_privs"); + } + + return local.result; + } + /** * Get CommandBox Home directory by running info --json command * Returns CLIHome path or empty string if not found From a756434dc00a541b69c358f696433c60e45c4016 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 22 Jan 2026 18:59:17 +0500 Subject: [PATCH 016/405] commit: update docker commands status messages --- .../commands/wheels/docker/DockerCommand.cfc | 64 ++++--- cli/src/commands/wheels/docker/build.cfc | 124 ++++++------ cli/src/commands/wheels/docker/deploy.cfc | 177 +++++++++--------- cli/src/commands/wheels/docker/exec.cfc | 30 ++- cli/src/commands/wheels/docker/login.cfc | 8 +- cli/src/commands/wheels/docker/logs.cfc | 42 ++--- cli/src/commands/wheels/docker/push.cfc | 123 ++++++------ cli/src/commands/wheels/docker/stop.cfc | 107 +++++------ 8 files changed, 332 insertions(+), 343 deletions(-) diff --git a/cli/src/commands/wheels/docker/DockerCommand.cfc b/cli/src/commands/wheels/docker/DockerCommand.cfc index fa4aeef21b..f22dd76aff 100644 --- a/cli/src/commands/wheels/docker/DockerCommand.cfc +++ b/cli/src/commands/wheels/docker/DockerCommand.cfc @@ -2,6 +2,8 @@ * Base component for Docker commands */ component extends="../base" { + + property name="detailOutput" inject="DetailOutputService@wheels-cli"; /** * Login to container registry @@ -28,14 +30,14 @@ component extends="../base" { switch(lCase(arguments.registry)) { case "dockerhub": if (!len(trim(local.username))) { - print.line("Enter Docker Hub username:"); + detailOutput.output("Enter Docker Hub username:"); local.username = ask(""); } - print.yellowLine("Logging in to Docker Hub...").toConsole(); + detailOutput.output("Logging in to Docker Hub..."); if (!len(trim(local.password))) { - print.line("Enter Docker Hub password or access token:"); + detailOutput.output("Enter Docker Hub password or access token:"); local.password = ask(message="", mask="*"); } @@ -49,8 +51,8 @@ component extends="../base" { break; case "ecr": - print.yellowLine("Logging in to AWS ECR...").toConsole(); - print.cyanLine("Note: AWS CLI must be configured with valid credentials").toConsole(); + detailOutput.output("Logging in to AWS ECR..."); + detailOutput.invoke("Note: AWS CLI must be configured with valid credentials"); // Extract region from image name if (!len(trim(arguments.image))) { @@ -58,7 +60,7 @@ component extends="../base" { } var region = extractAWSRegion(arguments.image); - print.cyanLine("Detected region: " & region).toConsole(); + detailOutput.identical("Detected region: " & region); if (arguments.isLocal) { // aws ecr get-login-password --region region | docker login --username AWS --password-stdin account.dkr.ecr.region.amazonaws.com @@ -85,14 +87,14 @@ component extends="../base" { break; case "gcr": - print.yellowLine("Logging in to Google Container Registry...").toConsole(); + detailOutput.create("Logging in to Google Container Registry..."); local.keyFile = ""; if (fileExists(getCWD() & "/gcr-key.json")) { local.keyFile = getCWD() & "/gcr-key.json"; - print.cyanLine("Found service account key: gcr-key.json").toConsole(); + detailOutput.statusSuccess("Found service account key: gcr-key.json"); } else { - print.line("Enter path to service account key file (JSON):"); + detailOutput.output("Enter path to service account key file (JSON):"); local.keyFile = ask(message=""); } @@ -127,7 +129,7 @@ component extends="../base" { local.image = deployConfig.image; local.registryUrl = listFirst(local.image, "/"); } else { - print.line("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); + detailOutput.output("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); local.registryUrl = ask(message=""); if (!len(trim(local.registryUrl))) { error("Azure ACR requires a registry URL."); @@ -137,15 +139,15 @@ component extends="../base" { // 2. Resolve Username if (!len(trim(local.username))) { - print.line("Enter Azure ACR username:"); + detailOutput.output("Enter Azure ACR username:"); local.username = ask(""); } - print.yellowLine("Logging in to Azure Container Registry: #local.registryUrl#").toConsole(); + detailOutput.create("Logging in to Azure Container Registry: #local.registryUrl#"); // 3. Resolve Password - if (!len(trim(local.password))) { - print.line("Enter ACR password:"); + if (!len(trim(local.password))) { + detailOutput.output("Enter ACR password:"); local.password = ask(message="", mask="*"); } @@ -160,13 +162,13 @@ component extends="../base" { case "ghcr": if (!len(trim(local.username))) { - print.line("Enter GitHub username:"); + detailOutput.output("Enter GitHub username:"); local.username = ask(""); } - print.yellowLine("Logging in to GitHub Container Registry...").toConsole(); + detailOutput.create("Logging in to GitHub Container Registry..."); if (!len(trim(local.password))) { - print.line("Enter Personal Access Token (PAT) with write:packages scope:"); + detailOutput.output("Enter Personal Access Token (PAT) with write:packages scope:"); local.password = ask(message="", mask="*"); } @@ -190,7 +192,7 @@ component extends="../base" { local.image = deployConfig.image; local.registryUrl = listFirst(local.image, "/"); } else { - print.line("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); + detailOutput.output("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); local.registryUrl = ask(message=""); if (!len(trim(local.registryUrl))) { error("Private registry URL is required."); @@ -200,15 +202,15 @@ component extends="../base" { // 2. Resolve Username if (!len(trim(local.username))) { - print.line("Enter registry username:"); + detailOutput.output("Enter registry username:"); local.username = ask(""); } - print.yellowLine("Logging in to private registry: #local.registryUrl#").toConsole(); + detailOutput.create("Logging in to private registry: #local.registryUrl#"); // 3. Resolve Password if (!len(trim(local.password))) { - print.line("Enter registry password:"); + detailOutput.output("Enter registry password:"); local.password = ask(message="", mask="*"); } @@ -223,7 +225,7 @@ component extends="../base" { } if (arguments.isLocal) { - print.greenLine("Login successful").toConsole(); + detailOutput.statusSuccess("Login successful"); } return { @@ -419,7 +421,7 @@ component extends="../base" { if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image))) { local.registryUrl = listFirst(deployConfig.image, "/"); } else { - print.line("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); + detailOutput.output("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); local.registryUrl = ask(""); if (!len(trim(local.registryUrl))) { error("Azure ACR requires a registry URL to determine image path."); @@ -440,7 +442,7 @@ component extends="../base" { if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image))) { local.registryUrl = listFirst(deployConfig.image, "/"); } else { - print.line("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); + detailOutput.output("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); local.registryUrl = ask(""); if (!len(trim(local.registryUrl))) { error("Private registry requires a registry URL to determine image path."); @@ -487,7 +489,7 @@ component extends="../base" { if (isNull(local.line)) break; arrayAppend(local.outputParts, local.line); if (arguments.showOutput) { - print.line(local.line).toConsole(); + detailOutput.output(local.line); } } @@ -533,7 +535,7 @@ component extends="../base" { local.line = local.br.readLine(); if (isNull(local.line)) break; arrayAppend(local.outputParts, local.line); - print.line(local.line).toConsole(); + detailOutput.output(local.line); } local.exitCode = local.proc.waitFor(); @@ -627,7 +629,7 @@ component extends="../base" { public function testSSHConnection(string host, string user, numeric port) { var local = {}; - print.yellowLine("Testing SSH connection to " & arguments.host & "...").toConsole(); + detailOutput.output("Testing SSH connection to " & arguments.host & "..."); var sshCmd = ["ssh", "-p", arguments.port]; sshCmd.addAll(getSSHOptions()); sshCmd.addAll([arguments.user & "@" & arguments.host, "echo connected"]); @@ -678,7 +680,7 @@ component extends="../base" { var parts = listToArray(line, " " & chr(9), true); if (arrayLen(parts) < 2) { - print.yellowLine(" Skipping invalid line #lineNum#: #line#").toConsole(); + detailOutput.skip(" Skipping invalid line #lineNum#: #line#"); continue; } @@ -699,7 +701,7 @@ component extends="../base" { error("No valid servers found in text file"); } - print.greenLine("Loaded #arrayLen(servers)# server(s) from text file").toConsole(); + detailOutput.statusSuccess("Loaded #arrayLen(servers)# server(s) from text file"); return servers; } catch (any e) { @@ -750,7 +752,7 @@ component extends="../base" { } } - print.greenLine("Loaded #arrayLen(config.servers)# server(s) from config file").toConsole(); + detailOutput.statusSuccess("Loaded #arrayLen(config.servers)# server(s) from config file"); return config.servers; } catch (any e) { @@ -758,4 +760,4 @@ component extends="../base" { } } -} +} \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/build.cfc b/cli/src/commands/wheels/docker/build.cfc index 70c2cbd4f5..bcc4ab3969 100644 --- a/cli/src/commands/wheels/docker/build.cfc +++ b/cli/src/commands/wheels/docker/build.cfc @@ -10,7 +10,9 @@ * {code} */ component extends="DockerCommand" { - + + property name="detailOutput" inject="DetailOutputService@wheels-cli"; + /** * @local Build Docker image on local machine * @remote Build Docker image on remote server(s) @@ -55,10 +57,8 @@ component extends="DockerCommand" { // ============================================================================= private function buildLocal(string customTag, boolean nocache, boolean pull) { - print.line(); - print.boldMagentaLine("Wheels Docker Local Build"); - print.line(); - + detailOutput.header("Wheels Docker Local Build"); + // Check if Docker is installed locally if (!isDockerInstalled()) { error("Docker is not installed or not accessible. Please ensure Docker Desktop or Docker Engine is running."); @@ -68,7 +68,7 @@ component extends="DockerCommand" { local.useCompose = hasDockerComposeFile(); if (local.useCompose) { - print.greenLine("Found docker-compose file, will build using docker-compose").toConsole(); + detailOutput.statusSuccess("Found docker-compose file, will build using docker-compose"); // Build command array local.buildCmd = ["docker", "compose", "build"]; @@ -81,15 +81,15 @@ component extends="DockerCommand" { arrayAppend(local.buildCmd, "--pull"); } - print.yellowLine("Building services with docker-compose...").toConsole(); + detailOutput.output("Building services with docker-compose..."); runLocalCommand(local.buildCmd); - print.line(); - print.boldGreenLine("Docker Compose services built successfully!").toConsole(); - print.line(); - print.yellowLine("View images with: docker images").toConsole(); - print.yellowLine("Start services with: wheels docker deploy --local").toConsole(); - print.line(); + detailOutput.line(); + detailOutput.statusSuccess("Docker Compose services built successfully!"); + detailOutput.line(); + detailOutput.output("View images with: docker images"); + detailOutput.output("Start services with: wheels docker deploy --local"); + detailOutput.line(); } else { // Check for Dockerfile @@ -98,7 +98,7 @@ component extends="DockerCommand" { error("No Dockerfile or docker-compose.yml found in current directory"); } - print.greenLine("Found Dockerfile, will build using standard docker build").toConsole(); + detailOutput.statusSuccess("Found Dockerfile, will build using standard docker build"); // Get project name and determine tag local.projectName = getProjectName(); @@ -115,7 +115,7 @@ component extends="DockerCommand" { local.imageTag = local.baseImageName & ":latest"; } - print.cyanLine("Building image: " & local.imageTag).toConsole(); + detailOutput.statusInfo("Building image: " & local.imageTag); // Build command array local.buildCmd = ["docker", "build", "-t", local.imageTag]; @@ -130,16 +130,16 @@ component extends="DockerCommand" { arrayAppend(local.buildCmd, "."); - print.yellowLine("Building Docker image...").toConsole(); + detailOutput.output("Building Docker image..."); runLocalCommand(local.buildCmd); - print.line(); - print.boldGreenLine("Docker image built successfully!").toConsole(); - print.line(); - print.yellowLine("Image tag: " & local.imageTag).toConsole(); - print.yellowLine("View image with: docker images " & local.projectName).toConsole(); - print.yellowLine("Run container with: wheels docker deploy --local").toConsole(); - print.line(); + detailOutput.line(); + detailOutput.statusSuccess("Docker image built successfully!"); + detailOutput.line(); + detailOutput.output("Image tag: " & local.imageTag); + detailOutput.output("View image with: docker images " & local.projectName); + detailOutput.output("Run container with: wheels docker deploy --local"); + detailOutput.line(); } } @@ -183,7 +183,7 @@ component extends="DockerCommand" { if (isNull(local.line)) break; arrayAppend(local.outputParts, local.line); if (arguments.showOutput) { - print.line(local.line).toConsole(); + detailOutput.output(local.line); } } @@ -213,7 +213,7 @@ component extends="DockerCommand" { if (len(trim(arguments.serverNumbers)) == 0 && fileExists(ymlConfigPath)) { var deployConfig = getDeployConfig(); if (arrayLen(deployConfig.servers)) { - print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + detailOutput.identical("Found config/deploy.yml, loading server configuration"); allServers = deployConfig.servers; // Add default remoteDir if not present @@ -231,11 +231,11 @@ component extends="DockerCommand" { if (arrayLen(serversToBuild) == 0) { if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.txt, loading server configuration"); allServers = loadServersFromTextFile("deploy-servers.txt"); serversToBuild = filterServers(allServers, arguments.serverNumbers); } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.json, loading server configuration"); allServers = loadServersFromConfig("deploy-servers.json"); serversToBuild = filterServers(allServers, arguments.serverNumbers); } else { @@ -247,12 +247,14 @@ component extends="DockerCommand" { error("No servers configured for building"); } - print.line().boldCyanLine("Building Docker images on #arrayLen(serversToBuild)# server(s)...").toConsole(); + detailOutput.line(); + detailOutput.create("Building Docker images on #arrayLen(serversToBuild)# server(s)..."); // Build on all selected servers buildOnServers(serversToBuild, arguments.customTag, arguments.nocache, arguments.pull); - print.line().boldGreenLine("Build operations completed on all servers!").toConsole(); + detailOutput.line(); + detailOutput.statusSuccess("Build operations completed on all servers!"); } /** @@ -272,16 +274,16 @@ component extends="DockerCommand" { if (num > 0 && num <= arrayLen(arguments.allServers)) { arrayAppend(selectedServers, arguments.allServers[num]); } else { - print.yellowLine("Skipping invalid server number: " & numStr).toConsole(); + detailOutput.skip("Skipping invalid server number: " & numStr); } } if (arrayLen(selectedServers) == 0) { - print.yellowLine("No valid servers selected, using all servers").toConsole(); + detailOutput.statusFailed("No valid servers selected, using all servers"); return arguments.allServers; } - print.greenLine("Selected #arrayLen(selectedServers)# of #arrayLen(arguments.allServers)# server(s)").toConsole(); + detailOutput.statusSuccess("Selected #arrayLen(selectedServers)# of #arrayLen(arguments.allServers)# server(s)"); return selectedServers; } @@ -295,25 +297,23 @@ component extends="DockerCommand" { for (var i = 1; i <= arrayLen(servers); i++) { serverConfig = servers[i]; - print.line().boldCyanLine("---------------------------------------").toConsole(); - print.boldCyanLine("Building on server #i# of #arrayLen(servers)#: #serverConfig.host#").toConsole(); - print.line().boldCyanLine("---------------------------------------").toConsole(); + detailOutput.header("Building on server #i# of #arrayLen(servers)#: #serverConfig.host#"); try { buildOnServer(serverConfig, arguments.customTag, arguments.nocache, arguments.pull); successCount++; - print.greenLine("Build on #serverConfig.host# completed successfully").toConsole(); + detailOutput.statusSuccess("Build on #serverConfig.host# completed successfully"); } catch (any e) { failureCount++; - print.redLine("Failed to build on #serverConfig.host#: #e.message#").toConsole(); + detailOutput.statusFailed("Failed to build on #serverConfig.host#: #e.message#"); } } - print.line().toConsole(); - print.boldCyanLine("Build Operations Summary:").toConsole(); - print.greenLine(" Successful: #successCount#").toConsole(); + detailOutput.line(); + detailOutput.output("Build Operations Summary:"); + detailOutput.statusSuccess(" Successful: #successCount#"); if (failureCount > 0) { - print.redLine(" Failed: #failureCount#").toConsole(); + detailOutput.statusFailed(" Failed: #failureCount#"); } } @@ -332,19 +332,19 @@ component extends="DockerCommand" { if (!testSSHConnection(local.host, local.user, local.port)) { error("SSH connection failed to #local.host#. Check credentials and access."); } - print.greenLine("SSH connection successful").toConsole(); + detailOutput.statusSuccess("SSH connection successful"); // Check if remote directory exists - print.yellowLine("Checking remote directory...").toConsole(); + detailOutput.output("Checking remote directory..."); local.checkDirCmd = "test -d " & local.remoteDir; local.dirExists = false; try { executeRemoteCommand(local.host, local.user, local.port, local.checkDirCmd); local.dirExists = true; - print.greenLine("Remote directory exists").toConsole(); + detailOutput.statusSuccess("Remote directory exists"); } catch (any e) { - print.yellowLine("Remote directory does not exist, uploading source code...").toConsole(); + detailOutput.output("Remote directory does not exist, uploading source code..."); uploadSourceCode(local.host, local.user, local.port, local.remoteDir); } @@ -355,15 +355,15 @@ component extends="DockerCommand" { try { executeRemoteCommand(local.host, local.user, local.port, local.checkComposeCmd); local.useCompose = true; - print.greenLine("Found docker-compose file on remote server").toConsole(); + detailOutput.statusSuccess("Found docker-compose file on remote server"); } catch (any e) { - print.yellowLine("No docker-compose file found, checking for Dockerfile...").toConsole(); + detailOutput.output("No docker-compose file found, checking for Dockerfile..."); // Check if Dockerfile exists local.checkDockerfileCmd = "test -f " & local.remoteDir & "/Dockerfile"; try { executeRemoteCommand(local.host, local.user, local.port, local.checkDockerfileCmd); - print.greenLine("Found Dockerfile on remote server").toConsole(); + detailOutput.statusSuccess("Found Dockerfile on remote server"); } catch (any e2) { error("No Dockerfile or docker-compose.yml found on remote server in: " & local.remoteDir); } @@ -371,7 +371,7 @@ component extends="DockerCommand" { if (local.useCompose) { // Build using docker-compose - print.yellowLine("Building with docker-compose...").toConsole(); + detailOutput.output("Building with docker-compose..."); local.buildCmd = "cd " & local.remoteDir & " && "; @@ -400,11 +400,11 @@ component extends="DockerCommand" { local.buildCmd &= "; fi"; executeRemoteCommand(local.host, local.user, local.port, local.buildCmd); - print.greenLine("Docker Compose build completed").toConsole(); + detailOutput.statusSuccess("Docker Compose build completed"); } else { // Build using standard docker build - print.yellowLine("Building Docker image...").toConsole(); + detailOutput.create("Building Docker image..."); // Determine tag local.projectName = getProjectName(); @@ -420,7 +420,7 @@ component extends="DockerCommand" { } else { local.imageTag = local.baseImageName & ":latest"; } - print.cyanLine("Building image: " & local.imageTag).toConsole(); + detailOutput.create("Building image: " & local.imageTag); local.buildCmd = "cd " & local.remoteDir & " && "; @@ -451,10 +451,10 @@ component extends="DockerCommand" { local.buildCmd &= "; fi"; executeRemoteCommand(local.host, local.user, local.port, local.buildCmd); - print.greenLine("Docker image built: " & local.imageTag).toConsole(); + detailOutput.statusSuccess("Docker image built: " & local.imageTag); } - print.boldGreenLine("Build operations on #local.host# completed!").toConsole(); + detailOutput.success("Build operations on #local.host# completed!"); } /** @@ -463,7 +463,7 @@ component extends="DockerCommand" { private function uploadSourceCode(string host, string user, numeric port, string remoteDir) { var local = {}; - print.yellowLine("Creating deployment directory on remote server...").toConsole(); + detailOutput.create("Creating deployment directory on remote server..."); // Create remote directory local.createDirCmd = "sudo mkdir -p " & arguments.remoteDir & " && sudo chown -R $USER:$USER " & arguments.remoteDir; @@ -471,7 +471,7 @@ component extends="DockerCommand" { try { executeRemoteCommand(arguments.host, arguments.user, arguments.port, local.createDirCmd); } catch (any e) { - print.yellowLine("Note: Creating directory without sudo...").toConsole(); + detailOutput.output("Note: Creating directory without sudo..."); executeRemoteCommand(arguments.host, arguments.user, arguments.port, "mkdir -p " & arguments.remoteDir); } @@ -480,18 +480,18 @@ component extends="DockerCommand" { local.tarFile = getTempFile(getTempDirectory(), "buildsrc_") & ".tar.gz"; local.remoteTar = "/tmp/buildsrc_" & local.timestamp & ".tar.gz"; - print.yellowLine("Creating source tarball...").toConsole(); + detailOutput.create("Creating source tarball..."); runProcess(["tar", "-czf", local.tarFile, "-C", getCWD(), "."]); - print.yellowLine("Uploading source code to remote server...").toConsole(); + detailOutput.create("Uploading source code to remote server..."); runProcess(["scp", "-P", arguments.port, local.tarFile, arguments.user & "@" & arguments.host & ":" & local.remoteTar]); fileDelete(local.tarFile); - print.yellowLine("Extracting source code...").toConsole(); + detailOutput.create("Extracting source code..."); local.extractCmd = "tar -xzf " & local.remoteTar & " -C " & arguments.remoteDir & " && rm " & local.remoteTar; executeRemoteCommand(arguments.host, arguments.user, arguments.port, local.extractCmd); - print.greenLine("Source code uploaded successfully").toConsole(); + detailOutput.statusSuccess("Source code uploaded successfully"); } /** @@ -587,7 +587,7 @@ component extends="DockerCommand" { private function testSSHConnection(string host, string user, numeric port) { var local = {}; - print.yellowLine("Testing SSH connection to " & arguments.host & "...").toConsole(); + detailOutput.output("Testing SSH connection to " & arguments.host & "..."); local.result = runProcess([ "ssh", "-o", "BatchMode=yes", @@ -603,7 +603,7 @@ component extends="DockerCommand" { private function executeRemoteCommand(string host, string user, numeric port, string cmd) { var local = {}; - print.yellowLine("Running: ssh -p " & arguments.port & " " & arguments.user & "@" & arguments.host & " " & arguments.cmd).toConsole(); + detailOutput.output("Running: ssh -p " & arguments.port & " " & arguments.user & "@" & arguments.host & " " & arguments.cmd); local.result = runProcess([ "ssh", diff --git a/cli/src/commands/wheels/docker/deploy.cfc b/cli/src/commands/wheels/docker/deploy.cfc index 2bc8fc3cac..06150538b6 100644 --- a/cli/src/commands/wheels/docker/deploy.cfc +++ b/cli/src/commands/wheels/docker/deploy.cfc @@ -10,6 +10,8 @@ * {code} */ component extends="DockerCommand" { + + property name="detailOutput" inject="DetailOutputService@wheels-cli"; /** * @local Deploy to local Docker environment @@ -76,27 +78,27 @@ component extends="DockerCommand" { // (CFML doesn't have a native Set, so we can use a struct key trick or just leave it if docker output is unique enough) if (arrayLen(candidates) > 1) { - print.line().toConsole(); - print.boldCyanLine("Select a tag to deploy for project '#projectName#':").toConsole(); + detailOutput.line(); + detailOutput.output("Select a tag to deploy for project '#projectName#':"); for (var i=1; i<=arrayLen(candidates); i++) { - print.line(" #i#. " & candidates[i]).toConsole(); + detailOutput.output(" #i#. " & candidates[i]); } - print.line().toConsole(); + detailOutput.line(); var selection = ask("Enter number to select, or press Enter for 'latest': "); if (len(trim(selection)) && isNumeric(selection) && selection > 0 && selection <= arrayLen(candidates)) { - arguments.tag = candidates[selection]; - print.greenLine("Selected tag: " & arguments.tag).toConsole(); + arguments.tag = candidates[selection]; + detailOutput.statusSuccess("Selected tag: " & arguments.tag); } else if (len(trim(selection))) { - // Treat as custom tag input if they typed a string not in the list? - // Or just fallback to what they typed - arguments.tag = selection; - print.greenLine("Using custom tag: " & arguments.tag).toConsole(); + // Treat as custom tag input if they typed a string not in the list? + // Or just fallback to what they typed + arguments.tag = selection; + detailOutput.statusSuccess("Using custom tag: " & arguments.tag); } else { // Empty selection matches 'latest' default logic later, or we can explicit set it - print.yellowLine("No selection made, defaulting to 'latest'").toConsole(); + detailOutput.output("No selection made, defaulting to 'latest'"); } } } @@ -135,29 +137,27 @@ component extends="DockerCommand" { string tag="" ) { // Welcome message - print.line(); - print.boldMagentaLine("Wheels Docker Local Deployment"); - print.line(); + detailOutput.header("Wheels Docker Local Deployment"); // Check for docker-compose file local.useCompose = hasDockerComposeFile(); if (local.useCompose) { - print.greenLine("Found docker-compose file, will use docker-compose").toConsole(); + detailOutput.statusSuccess("Found docker-compose file, will use docker-compose"); // Just run docker-compose up if (len(arguments.tag)) { - print.yellowLine("Note: --tag argument is ignored when using docker-compose.").toConsole(); + detailOutput.output("Note: --tag argument is ignored when using docker-compose."); } - print.yellowLine("Starting services...").toConsole(); + detailOutput.output("Starting services..."); runLocalCommand(["docker-compose", "up", "-d", "--build"]); - print.line(); - print.boldGreenLine("Services started successfully!").toConsole(); - print.line(); - print.yellowLine("View logs with: docker-compose logs -f").toConsole(); - print.line(); + detailOutput.line(); + detailOutput.statusSuccess("Services started successfully!"); + detailOutput.line(); + detailOutput.output("View logs with: docker-compose logs -f"); + detailOutput.line(); } else { // Check for Dockerfile @@ -166,7 +166,7 @@ component extends="DockerCommand" { error("No Dockerfile or docker-compose.yml found in current directory"); } - print.greenLine("Found Dockerfile, will use standard docker commands").toConsole(); + detailOutput.statusSuccess("Found Dockerfile, will use standard docker commands"); // Check if Docker is installed locally if (!isDockerInstalled()) { @@ -176,10 +176,10 @@ component extends="DockerCommand" { // Extract port from Dockerfile local.exposedPort = getDockerExposedPort(); if (!len(local.exposedPort)) { - print.yellowLine("No EXPOSE directive found in Dockerfile, using default port 8080").toConsole(); + detailOutput.output("No EXPOSE directive found in Dockerfile, using default port 8080"); local.exposedPort = "8080"; } else { - print.greenLine("Found EXPOSE port: " & local.exposedPort).toConsole(); + detailOutput.statusSuccess("Found EXPOSE port: " & local.exposedPort); } // Get project name for image/container naming @@ -201,32 +201,32 @@ component extends="DockerCommand" { // Container Name: Always use project name for consistency local.containerName = local.projectName; - print.yellowLine("Building Docker image (" & local.imageName & ")...").toConsole(); + detailOutput.create("Building Docker image (" & local.imageName & ")..."); runLocalCommand(["docker", "build", "-t", local.imageName, "."]); - print.yellowLine("Starting container...").toConsole(); + detailOutput.output("Starting container..."); try { // Stop and remove existing container runLocalCommand(["docker", "stop", local.containerName]); runLocalCommand(["docker", "rm", local.containerName]); } catch (any e) { - print.yellowLine("No existing container to remove").toConsole(); + detailOutput.output("No existing container to remove"); } // Run new container runLocalCommand(["docker", "run", "-d", "--name", local.containerName, "-p", local.exposedPort & ":" & local.exposedPort, local.imageName]); - print.line(); - print.boldGreenLine("Container started successfully!").toConsole(); - print.line(); - print.yellowLine("Image: " & local.imageName).toConsole(); - print.yellowLine("Container: " & local.containerName).toConsole(); - print.yellowLine("Access your application at: http://localhost:" & local.exposedPort).toConsole(); - print.line(); - print.yellowLine("Check container status with: docker ps").toConsole(); - print.yellowLine("View logs with: wheels docker logs --local").toConsole(); - print.line(); + detailOutput.line(); + detailOutput.statusSuccess("Container started successfully!"); + detailOutput.line(); + detailOutput.output("Image: " & local.imageName); + detailOutput.output("Container: " & local.containerName); + detailOutput.output("Access your application at: http://localhost:" & local.exposedPort); + detailOutput.line(); + detailOutput.output("Check container status with: docker ps"); + detailOutput.output("View logs with: wheels docker logs --local"); + detailOutput.line(); } } @@ -271,7 +271,7 @@ component extends="DockerCommand" { else if (fileExists(ymlConfigPath)) { var deployConfig = getDeployConfig(); if (arrayLen(deployConfig.servers)) { - print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + detailOutput.identical("Found config/deploy.yml, loading server configuration"); servers = deployConfig.servers; // Add defaults for missing fields @@ -287,10 +287,10 @@ component extends="DockerCommand" { } // 2. Otherwise, look for default files else if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.txt, loading server configuration"); servers = loadServersFromTextFile("deploy-servers.txt"); } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.json, loading server configuration"); servers = loadServersFromConfig("deploy-servers.json"); } else { error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); @@ -300,15 +300,16 @@ component extends="DockerCommand" { error("No servers configured for deployment"); } - print.line().boldCyanLine("Starting remote deployment to #arrayLen(servers)# server(s)...").toConsole(); + detailOutput.identical("Starting remote deployment to #arrayLen(servers)# server(s)..."); if (arguments.blueGreen) { - print.boldMagentaLine("Strategy: Blue/Green Deployment (Zero Downtime)").toConsole(); + detailOutput.output("Strategy: Blue/Green Deployment (Zero Downtime)"); } // Deploy to all servers sequentially deployToMultipleServersSequential(servers, arguments.skipDockerCheck, arguments.blueGreen, arguments.tag); - print.line().boldGreenLine("Deployment to all servers completed!").toConsole(); + detailOutput.line(); + detailOutput.success("Deployment to all servers completed!"); } /** @@ -330,9 +331,7 @@ component extends="DockerCommand" { serverConfig.tag = "latest"; } - print.line().boldCyanLine("---------------------------------------").toConsole(); - print.boldCyanLine("Deploying to server #i# of #arrayLen(servers)#: #serverConfig.host#").toConsole(); - print.line().boldCyanLine("---------------------------------------").toConsole(); + detailOutput.header("Deploying to server #i# of #arrayLen(servers)#: #serverConfig.host#"); try { if (arguments.blueGreen) { @@ -341,10 +340,10 @@ component extends="DockerCommand" { deployToSingleServer(serverConfig, arguments.skipDockerCheck); } successCount++; - print.greenLine("Server #serverConfig.host# deployed successfully").toConsole(); + detailOutput.statusSuccess("Server #serverConfig.host# deployed successfully"); } catch (any e) { failureCount++; - print.redLine("Failed to deploy to #serverConfig.host#: #e.message#").toConsole(); + detailOutput.statusFailed("Failed to deploy to #serverConfig.host#: #e.message#"); } } } @@ -379,31 +378,31 @@ component extends="DockerCommand" { if (!testSSHConnection(local.host, local.user, local.port)) { error("SSH connection failed to #local.host#. Check credentials and access."); } - print.greenLine("SSH connection successful").toConsole(); + detailOutput.statusSuccess("SSH connection successful"); // Step 1.5: Check and install Docker if needed (unless skipped) if (!arguments.skipDockerCheck) { ensureDockerInstalled(local.host, local.user, local.port); } else { - print.yellowLine("Skipping Docker installation check (--skipDockerCheck flag is set)").toConsole(); + detailOutput.output("Skipping Docker installation check (--skipDockerCheck flag is set)"); } // Step 2: Create remote directory - print.yellowLine("Creating remote directory...").toConsole(); + detailOutput.create("Creating remote directory..."); executeRemoteCommand(local.host, local.user, local.port, "mkdir -p " & local.remoteDir); // Step 3: Check for docker-compose file local.useCompose = hasDockerComposeFile(); if (local.useCompose) { - print.greenLine("Found docker-compose file, will use docker-compose").toConsole(); + detailOutput.statusSuccess("Found docker-compose file, will use docker-compose"); } else { // Extract port from Dockerfile for standard docker run local.exposedPort = getDockerExposedPort(); if (!len(local.exposedPort)) { - print.yellowLine(" No EXPOSE directive found in Dockerfile, using default port 8080").toConsole(); + detailOutput.output(" No EXPOSE directive found in Dockerfile, using default port 8080"); local.exposedPort = "8080"; } else { - print.greenLine("Found EXPOSE port: " & local.exposedPort).toConsole(); + detailOutput.statusSuccess("Found EXPOSE port: " & local.exposedPort); } } @@ -412,10 +411,10 @@ component extends="DockerCommand" { local.tarFile = getTempFile(getTempDirectory(), "deploysrc_") & ".tar.gz"; local.remoteTar = "/tmp/deploysrc_" & local.timestamp & ".tar.gz"; - print.yellowLine("Creating source tarball...").toConsole(); + detailOutput.create("Creating source tarball..."); runLocalCommand(["tar", "-czf", local.tarFile, "-C", getCWD(), "."]); - print.yellowLine(" Uploading to remote server...").toConsole(); + detailOutput.output(" Uploading to remote server..."); var scpCmd = ["scp", "-P", local.port]; scpCmd.addAll(getSSHOptions()); scpCmd.addAll([local.tarFile, local.user & "@" & local.host & ":" & local.remoteTar]); @@ -469,14 +468,14 @@ component extends="DockerCommand" { local.tempFile = getTempFile(getTempDirectory(), "deploy_"); fileWrite(local.tempFile, local.deployScript); - print.yellowLine("Uploading deployment script...").toConsole(); + detailOutput.output("Uploading deployment script..."); var scpScriptCmd = ["scp", "-P", local.port]; scpScriptCmd.addAll(getSSHOptions()); scpScriptCmd.addAll([local.tempFile, local.user & "@" & local.host & ":/tmp/deploy-simple.sh"]); runLocalCommand(scpScriptCmd); fileDelete(local.tempFile); - print.yellowLine("Executing deployment script remotely...").toConsole(); + detailOutput.output("Executing deployment script remotely..."); // Use interactive command var execCmd = ["ssh", "-p", local.port]; execCmd.addAll(getSSHOptions()); @@ -484,7 +483,7 @@ component extends="DockerCommand" { runInteractiveCommand(execCmd); - print.boldGreenLine("Deployment to #local.host# completed successfully!").toConsole(); + detailOutput.success("Deployment to #local.host# completed successfully!"); } /** @@ -505,7 +504,7 @@ component extends="DockerCommand" { if (!testSSHConnection(local.host, local.user, local.port)) { error("SSH connection failed to #local.host#. Check credentials and access."); } - print.greenLine("SSH connection successful").toConsole(); + detailOutput.statusSuccess("SSH connection successful"); // Step 1.5: Check and install Docker if (!arguments.skipDockerCheck) { @@ -513,16 +512,16 @@ component extends="DockerCommand" { } // Step 2: Create remote directory - print.yellowLine("Creating remote directory...").toConsole(); + detailOutput.create("Creating remote directory..."); executeRemoteCommand(local.host, local.user, local.port, "mkdir -p " & local.remoteDir); // Step 3: Determine Port local.exposedPort = getDockerExposedPort(); if (!len(local.exposedPort)) { - print.yellowLine(" No EXPOSE directive found in Dockerfile, using default port 8080").toConsole(); + detailOutput.output(" No EXPOSE directive found in Dockerfile, using default port 8080"); local.exposedPort = "8080"; } else { - print.greenLine("Found EXPOSE port: " & local.exposedPort).toConsole(); + detailOutput.statusSuccess("Found EXPOSE port: " & local.exposedPort); } // Step 4: Tar and upload project @@ -530,10 +529,10 @@ component extends="DockerCommand" { local.tarFile = getTempFile(getTempDirectory(), "deploysrc_") & ".tar.gz"; local.remoteTar = "/tmp/deploysrc_" & local.timestamp & ".tar.gz"; - print.yellowLine("Creating source tarball...").toConsole(); + detailOutput.create("Creating source tarball..."); runLocalCommand(["tar", "-czf", local.tarFile, "-C", getCWD(), "."]); - print.yellowLine(" Uploading to remote server...").toConsole(); + detailOutput.output(" Uploading to remote server..."); var scpCmd = ["scp", "-P", local.port]; scpCmd.addAll(getSSHOptions()); scpCmd.addAll([local.tarFile, local.user & "@" & local.host & ":" & local.remoteTar]); @@ -625,21 +624,21 @@ component extends="DockerCommand" { local.tempFile = getTempFile(getTempDirectory(), "deploy_bg_"); fileWrite(local.tempFile, local.deployScript); - print.yellowLine("Uploading Blue/Green deployment script...").toConsole(); + detailOutput.output("Uploading Blue/Green deployment script..."); var scpScriptCmd = ["scp", "-P", local.port]; scpScriptCmd.addAll(getSSHOptions()); scpScriptCmd.addAll([local.tempFile, local.user & "@" & local.host & ":/tmp/deploy-bluegreen.sh"]); runLocalCommand(scpScriptCmd); fileDelete(local.tempFile); - print.yellowLine("Executing Blue/Green deployment script remotely...").toConsole(); + detailOutput.output("Executing Blue/Green deployment script remotely..."); var execCmd = ["ssh", "-p", local.port]; execCmd.addAll(getSSHOptions()); execCmd.addAll([local.user & "@" & local.host, "chmod +x /tmp/deploy-bluegreen.sh && bash /tmp/deploy-bluegreen.sh"]); runInteractiveCommand(execCmd); - print.boldGreenLine("Blue/Green Deployment to #local.host# completed successfully!").toConsole(); + detailOutput.success("Blue/Green Deployment to #local.host# completed successfully!"); } /** @@ -648,7 +647,7 @@ component extends="DockerCommand" { private function ensureDockerInstalled(string host, string user, numeric port) { var local = {}; - print.yellowLine("Checking Docker installation on remote server...").toConsole(); + detailOutput.output("Checking Docker installation on remote server..."); // Check if Docker is installed var checkCmd = ["ssh", "-p", arguments.port]; @@ -658,7 +657,7 @@ component extends="DockerCommand" { local.checkResult = runLocalCommand(checkCmd); if (local.checkResult.exitCode eq 0) { - print.greenLine("Docker is already installed").toConsole(); + detailOutput.statusSuccess("Docker is already installed"); // Get Docker version var versionCmd = ["ssh", "-p", arguments.port]; @@ -668,7 +667,7 @@ component extends="DockerCommand" { local.versionResult = runLocalCommand(versionCmd); if (local.versionResult.exitCode eq 0) { - print.cyanLine("Docker version: " & trim(local.versionResult.output)).toConsole(); + detailOutput.output("Docker version: " & trim(local.versionResult.output)); } // Check if docker compose is available @@ -677,10 +676,10 @@ component extends="DockerCommand" { return true; } - print.yellowLine("Docker is not installed. Attempting to install Docker...").toConsole(); + detailOutput.output("Docker is not installed. Attempting to install Docker..."); // Check if user has passwordless sudo access - print.yellowLine("Checking sudo access...").toConsole(); + detailOutput.output("Checking sudo access..."); var sudoCheckCmd = ["ssh", "-p", arguments.port]; sudoCheckCmd.addAll(getSSHOptions()); sudoCheckCmd.addAll([arguments.user & "@" & arguments.host, "sudo -n true 2>&1"]); @@ -688,12 +687,12 @@ component extends="DockerCommand" { local.sudoCheckResult = runLocalCommand(sudoCheckCmd); if (local.sudoCheckResult.exitCode neq 0) { - print.line().toConsole(); - print.boldRedLine("ERROR: User '#arguments.user#' does not have passwordless sudo access on #arguments.host#!").toConsole(); + detailOutput.line(); + detailOutput.statusFailed("ERROR: User '#arguments.user#' does not have passwordless sudo access on #arguments.host#!"); error("Cannot install Docker: User '" & arguments.user & "' requires passwordless sudo access on " & arguments.host); } - print.greenLine("User has sudo access").toConsole(); + detailOutput.statusSuccess("User has sudo access"); // Detect OS type var osCmd = ["ssh", "-p", arguments.port]; @@ -711,10 +710,10 @@ component extends="DockerCommand" { if (findNoCase("ubuntu", local.osResult.output) || findNoCase("debian", local.osResult.output)) { local.installScript = getDockerInstallScriptDebian(); - print.cyanLine("Detected Debian/Ubuntu system").toConsole(); + detailOutput.skip("Detected Debian/Ubuntu system"); } else if (findNoCase("centos", local.osResult.output) || findNoCase("rhel", local.osResult.output) || findNoCase("fedora", local.osResult.output)) { local.installScript = getDockerInstallScriptRHEL(); - print.cyanLine("Detected RHEL/CentOS/Fedora system").toConsole(); + detailOutput.skip("Detected RHEL/CentOS/Fedora system"); } else { error("Unsupported OS. Docker installation is only automated for Ubuntu/Debian and RHEL/CentOS/Fedora systems."); } @@ -729,7 +728,7 @@ component extends="DockerCommand" { fileWrite(local.tempFile, local.installScript); // Upload install script - print.yellowLine("Uploading Docker installation script...").toConsole(); + detailOutput.output("Uploading Docker installation script..."); var scpInstallCmd = ["scp", "-P", arguments.port]; scpInstallCmd.addAll(getSSHOptions()); scpInstallCmd.addAll([local.tempFile, arguments.user & "@" & arguments.host & ":/tmp/install-docker.sh"]); @@ -737,7 +736,7 @@ component extends="DockerCommand" { fileDelete(local.tempFile); // Execute install script - print.yellowLine("Installing Docker...").toConsole(); + detailOutput.output("Installing Docker..."); var installCmd = ["ssh", "-p", arguments.port]; installCmd.addAll(getSSHOptions()); installCmd.addAll(["-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=10"]); @@ -749,7 +748,7 @@ component extends="DockerCommand" { error("Failed to install Docker on remote server"); } - print.boldGreenLine("Docker installed successfully!").toConsole(); + detailOutput.statusSuccess("Docker installed successfully!"); // Verify installation var verifyCmd = ["ssh", "-p", arguments.port]; @@ -759,7 +758,7 @@ component extends="DockerCommand" { local.verifyResult = runLocalCommand(verifyCmd); if (local.verifyResult.exitCode eq 0) { - print.greenLine("Docker version: " & trim(local.verifyResult.output)).toConsole(); + detailOutput.statusSuccess("Docker version: " & trim(local.verifyResult.output)); } return true; @@ -779,8 +778,8 @@ component extends="DockerCommand" { local.composeResult = runLocalCommand(composeCmd); if (local.composeResult.exitCode eq 0) { - print.greenLine("Docker Compose is available").toConsole(); - print.cyanLine("Compose version: " & trim(local.composeResult.output)).toConsole(); + detailOutput.statusSuccess("Docker Compose is available"); + detailOutput.output("Compose version: " & trim(local.composeResult.output)); return true; } @@ -792,12 +791,12 @@ component extends="DockerCommand" { local.oldComposeResult = runLocalCommand(oldComposeCmd); if (local.oldComposeResult.exitCode eq 0) { - print.greenLine("Docker Compose (standalone) is available").toConsole(); - print.cyanLine("Compose version: " & trim(local.oldComposeResult.output)).toConsole(); + detailOutput.statusSuccess("Docker Compose (standalone) is available"); + detailOutput.output("Compose version: " & trim(local.oldComposeResult.output)); return true; } - print.yellowLine("Docker Compose is not available, but docker compose plugin should be included in modern Docker installations").toConsole(); + detailOutput.output("Docker Compose is not available, but docker compose plugin should be included in modern Docker installations"); return false; } @@ -849,4 +848,4 @@ echo "Docker installation completed successfully!" '; return script; } -} +} \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/exec.cfc b/cli/src/commands/wheels/docker/exec.cfc index 39905549b7..53325a83f3 100644 --- a/cli/src/commands/wheels/docker/exec.cfc +++ b/cli/src/commands/wheels/docker/exec.cfc @@ -10,6 +10,8 @@ */ component extends="DockerCommand" { + property name="detailOutput" inject="DetailOutputService@wheels-cli"; + /** * @command Command to execute in container * @servers Specific servers to execute on (comma-separated list) @@ -63,7 +65,7 @@ component extends="DockerCommand" { else if (fileExists(ymlConfigPath)) { var deployConfig = getDeployConfig(); if (arrayLen(deployConfig.servers)) { - print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + detailOutput.identical("Found config/deploy.yml, loading server configuration"); serverList = deployConfig.servers; // Add defaults for missing fields @@ -82,10 +84,10 @@ component extends="DockerCommand" { } // 2. Otherwise, look for default files else if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.txt, loading server configuration"); serverList = loadServersFromTextFile("deploy-servers.txt"); } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.json, loading server configuration"); serverList = loadServersFromConfig("deploy-servers.json"); } else { error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); @@ -100,15 +102,11 @@ component extends="DockerCommand" { error("Cannot run interactive commands on multiple servers simultaneously. Please specify a single server using 'servers=host'."); } - print.line(); - print.boldMagentaLine("Wheels Deploy Remote Execution"); - print.line("==================================================").toConsole(); + detailOutput.header("Wheels Deploy Remote Execution"); for (var serverConfig in serverList) { if (arrayLen(serverList) > 1) { - print.line().toConsole(); - print.boldCyanLine("=== Server: #serverConfig.host# ===").toConsole(); - print.line().toConsole(); + detailOutput.subHeader("=== Server: #serverConfig.host# ==="); } try { @@ -116,11 +114,11 @@ component extends="DockerCommand" { } catch (any e) { // Check for UserInterruptException (CommandBox specific) or standard InterruptedException if (findNoCase("UserInterruptException", e.message) || findNoCase("InterruptedException", e.message) || (structKeyExists(e, "type") && findNoCase("UserInterruptException", e.type))) { - print.line().toConsole(); - print.yellowLine("Command interrupted by user.").toConsole(); + detailOutput.line(); + detailOutput.statusFailed("Command interrupted by user."); break; } - print.redLine("Failed to execute command on #serverConfig.host#: #e.message#").toConsole(); + detailOutput.statusFailed("Failed to execute command on #serverConfig.host#: #e.message#"); } } } @@ -215,9 +213,9 @@ component extends="DockerCommand" { execCmd.addAll([local.user & "@" & local.host, dockerCmd]); // 4. Execute - print.cyanLine("Executing: " & arguments.command).toConsole(); - print.cyanLine("Container: " & containerName).toConsole(); - print.line().toConsole(); + detailOutput.output("Executing: " & arguments.command); + detailOutput.output("Container: " & containerName); + detailOutput.output(); // Use runInteractiveCommand for both interactive and non-interactive // For non-interactive, it streams output nicely. @@ -229,4 +227,4 @@ component extends="DockerCommand" { } } -} +} \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/login.cfc b/cli/src/commands/wheels/docker/login.cfc index ffb9629af6..f5fb3d7503 100644 --- a/cli/src/commands/wheels/docker/login.cfc +++ b/cli/src/commands/wheels/docker/login.cfc @@ -8,6 +8,8 @@ */ component extends="DockerCommand" { + property name="detailOutput" inject="DetailOutputService@wheels-cli"; + /** * @registry Registry type: dockerhub, ecr, gcr, acr, ghcr, private (default: dockerhub) * @username Registry username (required for dockerhub, ghcr, private) @@ -67,9 +69,9 @@ component extends="DockerCommand" { try { var configPath = fileSystemUtil.resolvePath("docker-config.json"); fileWrite(configPath, serializeJSON(config)); - print.greenLine("Configuration saved to docker-config.json").toConsole(); + detailOutput.statusSuccess("Configuration saved to docker-config.json"); } catch (any e) { - print.yellowLine("Warning: Could not save configuration: #e.message#").toConsole(); + detailOutput.statusWarning("Warning: Could not save configuration: #e.message#"); } } -} +} \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/logs.cfc b/cli/src/commands/wheels/docker/logs.cfc index 69a654d8f3..abd796dcac 100644 --- a/cli/src/commands/wheels/docker/logs.cfc +++ b/cli/src/commands/wheels/docker/logs.cfc @@ -12,6 +12,8 @@ */ component extends="DockerCommand" { + property name="detailOutput" inject="DetailOutputService@wheels-cli"; + /** * @servers Specific servers to check (comma-separated list) * @tail Number of lines to show (default: 100) @@ -74,7 +76,7 @@ component extends="DockerCommand" { else if (fileExists(ymlConfigPath)) { var deployConfig = getDeployConfig(); if (arrayLen(deployConfig.servers)) { - print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + detailOutput.identical("Found config/deploy.yml, loading server configuration"); serverList = deployConfig.servers; // Add defaults for missing fields @@ -93,10 +95,10 @@ component extends="DockerCommand" { } // 2. Otherwise, look for default files else if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.txt, loading server configuration"); serverList = loadServersFromTextFile("deploy-servers.txt"); } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.json, loading server configuration"); serverList = loadServersFromConfig("deploy-servers.json"); } else { error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); @@ -111,15 +113,11 @@ component extends="DockerCommand" { error("Cannot follow logs from multiple servers simultaneously. Please specify a single server using 'servers=host'."); } - print.line(); - print.boldMagentaLine("Wheels Deployment Logs"); - print.line("==================================================").toConsole(); + detailOutput.header("Wheels Deployment Logs"); for (var serverConfig in serverList) { if (arrayLen(serverList) > 1) { - print.line().toConsole(); - print.boldCyanLine("=== Server: #serverConfig.host# ===").toConsole(); - print.line().toConsole(); + detailOutput.subHeader("=== Server: #serverConfig.host# ==="); } try { @@ -127,11 +125,11 @@ component extends="DockerCommand" { } catch (any e) { // Check for UserInterruptException (CommandBox specific) or standard InterruptedException if (findNoCase("UserInterruptException", e.message) || findNoCase("InterruptedException", e.message) || (structKeyExists(e, "type") && findNoCase("UserInterruptException", e.type))) { - print.line().toConsole(); - print.yellowLine("Command interrupted by user.").toConsole(); + detailOutput.line(); + detailOutput.statusFailed("Command interrupted by user."); break; } - print.redLine("Failed to fetch logs from #serverConfig.host#: #e.message#").toConsole(); + detailOutput.statusFailed("Failed to fetch logs from #serverConfig.host#: #e.message#"); } } } @@ -236,11 +234,11 @@ component extends="DockerCommand" { // However, runLocalCommand waits for completion. For -f, it will run indefinitely until user interrupts. // This is fine for CLI usage. - print.cyanLine("Fetching logs from container: " & containerName).toConsole(); + detailOutput.statusInfo("Fetching logs from container: " & containerName); if (arguments.follow) { - print.yellowLine("Following logs... (Press Ctrl+C to stop)").toConsole(); + detailOutput.output("Following logs... (Press Ctrl+C to stop)"); } - print.line("----------------------------------------").toConsole(); + detailOutput.line("----------------------------------------"); var result = runInteractiveCommand(logsCmd); @@ -301,15 +299,13 @@ component extends="DockerCommand" { error("Could not find running container for service: " & arguments.service); } - print.line(); - print.boldMagentaLine("Wheels Deployment Logs (Local)"); - print.line("==================================================").toConsole(); - print.cyanLine("Fetching logs from local container: " & containerName).toConsole(); - + detailOutput.header("Wheels Deployment Logs (Local)"); + detailOutput.statusInfo("Fetching logs from local container: " & containerName); + if (arguments.follow) { - print.yellowLine("Following logs... (Press Ctrl+C to stop)").toConsole(); + detailOutput.output("Following logs... (Press Ctrl+C to stop)"); } - print.line("----------------------------------------").toConsole(); + detailOutput.output("----------------------------------------"); // Construct Docker Logs Command var dockerCmd = ["docker", "logs"]; @@ -335,4 +331,4 @@ component extends="DockerCommand" { } } -} +} \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/push.cfc b/cli/src/commands/wheels/docker/push.cfc index 98d0551c09..0935004ef2 100644 --- a/cli/src/commands/wheels/docker/push.cfc +++ b/cli/src/commands/wheels/docker/push.cfc @@ -10,6 +10,8 @@ * {code} */ component extends="DockerCommand" { + + property name="detailOutput" inject="DetailOutputService@wheels-cli"; /** * @local Push image from local machine @@ -58,25 +60,25 @@ component extends="DockerCommand" { if (!len(trim(arguments.registry)) && structKeyExists(config, "registry")) { arguments.registry = config.registry; - print.cyanLine("Using registry from session: #arguments.registry#").toConsole(); + detailOutput.statusInfo("Using registry from session: #arguments.registry#"); } if (!len(trim(arguments.username)) && structKeyExists(config, "username")) { arguments.username = config.username; - print.cyanLine("Using username from session: #arguments.username#").toConsole(); + detailOutput.statusInfo("Using username from session: #arguments.username#"); } if (!len(trim(arguments.namespace)) && structKeyExists(config, "namespace")) { arguments.namespace = config.namespace; if (len(trim(arguments.namespace))) { - print.cyanLine("Using namespace from session: #arguments.namespace#").toConsole(); + detailOutput.statusInfo("Using namespace from session: #arguments.namespace#"); } } if (!len(trim(arguments.image)) && structKeyExists(config, "image")) { arguments.image = config.image; if (len(trim(arguments.image))) { - print.cyanLine("Using image from session: #arguments.image#").toConsole(); + detailOutput.statusInfo("Using image from session: #arguments.image#"); } } } catch (any e) {} @@ -120,9 +122,7 @@ component extends="DockerCommand" { // ============================================================================= private function pushLocal(string registry, string customImage, string username, string password, string tag, boolean build, string namespace) { - print.line(); - print.boldMagentaLine("Wheels Docker Push - Local"); - print.line(); + detailOutput.header("Wheels Docker Push - Local"); // Check if Docker is installed locally if (!isDockerInstalled()) { @@ -142,20 +142,20 @@ component extends="DockerCommand" { } } - print.cyanLine("Project: " & local.projectName).toConsole(); - print.cyanLine("Registry: " & arguments.registry).toConsole(); - print.line(); + detailOutput.statusInfo("Project: " & local.projectName); + detailOutput.statusInfo("Registry: " & arguments.registry); + detailOutput.line(); // Build image if requested if (arguments.build) { - print.yellowLine("Building image before push...").toConsole(); + detailOutput.output("Building image before push..."); buildLocalImage(); } // Check if local image exists if (!checkLocalImageExists(local.localImageName)) { - print.yellowLine("Local image '#local.localImageName#' not found.").toConsole(); - print.line("Would you like to build it now? (y/n)").toConsole(); + detailOutput.output("Local image '#local.localImageName#' not found."); + detailOutput.output("Would you like to build it now? (y/n)"); local.answer = ask(""); if (lCase(local.answer) == "y") { buildLocalImage(); @@ -164,7 +164,7 @@ component extends="DockerCommand" { } } - print.greenLine("Found local image: " & local.localImageName).toConsole(); + detailOutput.statusSuccess("Found local image: " & local.localImageName); // Determine final image name based on registry and user input local.finalImage = determineImageName( @@ -176,15 +176,15 @@ component extends="DockerCommand" { arguments.namespace ); - print.cyanLine("Target image: " & local.finalImage).toConsole(); - print.line(); + detailOutput.output("Target image: " & local.finalImage); + detailOutput.line(); // Tag the image if needed if (local.finalImage != local.localImageName) { - print.yellowLine("Tagging image: " & local.localImageName & " -> " & local.finalImage).toConsole(); + detailOutput.output("Tagging image: " & local.localImageName & " -> " & local.finalImage); try { runLocalCommand(["docker", "tag", local.localImageName, local.finalImage]); - print.greenLine("Image tagged successfully").toConsole(); + detailOutput.statusSuccess("Image tagged successfully"); } catch (any e) { error("Failed to tag image: " & e.message); } @@ -200,26 +200,26 @@ component extends="DockerCommand" { isLocal=true ); } else { - print.yellowLine("No password provided, attempting to push with existing credentials...").toConsole(); + detailOutput.statusWarning("No password provided, attempting to push with existing credentials..."); } // Push the image - print.yellowLine("Pushing image to " & arguments.registry & "...").toConsole(); + detailOutput.output("Pushing image to " & arguments.registry & "..."); try { runLocalCommand(["docker", "push", local.finalImage]); - print.line(); - print.boldGreenLine("Image pushed successfully to " & arguments.registry & "!").toConsole(); - print.line(); - print.yellowLine("Image: " & local.finalImage).toConsole(); - print.yellowLine("Pull with: docker pull " & local.finalImage).toConsole(); - print.line(); + detailOutput.line(); + detailOutput.statusSuccess("Image pushed successfully to " & arguments.registry & "!"); + detailOutput.line(); + detailOutput.output("Image: " & local.finalImage); + detailOutput.create("Pull with: docker pull " & local.finalImage); + detailOutput.line(); } catch (any e) { - print.redLine("Failed to push image: " & e.message).toConsole(); - print.line(); - print.yellowLine("You may need to login first.").toConsole(); - print.line("Try running: wheels docker login --registry=" & arguments.registry & " --username=" & arguments.username).toConsole(); - print.line("Or provide a password/token with --password").toConsole(); + detailOutput.statusFailed("Failed to push image: " & e.message); + detailOutput.line(); + detailOutput.output("You may need to login first."); + detailOutput.invoke("Try running: wheels docker login --registry=" & arguments.registry & " --username=" & arguments.username); + detailOutput.output("Or provide a password/token with --password"); error("Push failed"); } } @@ -228,8 +228,8 @@ component extends="DockerCommand" { * Build the local image */ private function buildLocalImage() { - print.yellowLine("Building Docker image...").toConsole(); - print.line(); + detailOutput.create("Building Docker image..."); + detailOutput.line(); // Check for docker-compose file local.useCompose = hasDockerComposeFile(); @@ -240,9 +240,8 @@ component extends="DockerCommand" { local.projectName = getProjectName(); runLocalCommand(["docker", "build", "-t", local.projectName & ":latest", "."]); } - - print.line(); - print.greenLine("Build completed successfully").toConsole(); + detailOutput.line(); + detailOutput.statusSuccess("Build completed successfully"); } private function hasDockerComposeFile() { @@ -271,7 +270,7 @@ component extends="DockerCommand" { if (len(trim(arguments.serverNumbers)) == 0 && fileExists(ymlConfigPath)) { var deployConfig = getDeployConfig(); if (arrayLen(deployConfig.servers)) { - print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + detailOutput.identical("Found config/deploy.yml, loading server configuration"); allServers = deployConfig.servers; serversToPush = allServers; } @@ -279,11 +278,11 @@ component extends="DockerCommand" { if (arrayLen(serversToPush) == 0) { if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.txt, loading server configuration"); allServers = loadServersFromTextFile("deploy-servers.txt"); serversToPush = filterServers(allServers, arguments.serverNumbers); } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.json, loading server configuration"); allServers = loadServersFromConfig("deploy-servers.json"); serversToPush = filterServers(allServers, arguments.serverNumbers); } else { @@ -295,12 +294,14 @@ component extends="DockerCommand" { error("No servers configured for pushing"); } - print.line().boldCyanLine("Pushing Docker images from #arrayLen(serversToPush)# server(s)...").toConsole(); - + detailOutput.line(); + detailOutput.statusInfo("Pushing Docker images from #arrayLen(serversToPush)# server(s)..."); + // Push from all selected servers pushFromServers(serversToPush, arguments.registry, arguments.image, arguments.username, arguments.password, arguments.tag, arguments.namespace); - print.line().boldGreenLine("Push operations completed on all servers!").toConsole(); + detailOutput.line(); + detailOutput.success("Push operations completed on all servers!"); } /** @@ -325,7 +326,7 @@ component extends="DockerCommand" { return arguments.allServers; } - print.greenLine("Selected #arrayLen(selectedServers)# of #arrayLen(arguments.allServers)# server(s)").toConsole(); + detailOutput.statusSuccess("Selected #arrayLen(selectedServers)# of #arrayLen(arguments.allServers)# server(s)"); return selectedServers; } @@ -338,25 +339,23 @@ component extends="DockerCommand" { for (var i = 1; i <= arrayLen(servers); i++) { var serverConfig = servers[i]; - print.line().boldCyanLine("---------------------------------------").toConsole(); - print.boldCyanLine("Pushing from server #i# of #arrayLen(servers)#: #serverConfig.host#").toConsole(); - print.line().boldCyanLine("---------------------------------------").toConsole(); + detailOutput.header("Pushing from server #i# of #arrayLen(servers)#: #serverConfig.host#"); try { pushFromServer(serverConfig, arguments.registry, arguments.image, arguments.username, arguments.password, arguments.tag, arguments.namespace); successCount++; - print.greenLine("Push from #serverConfig.host# completed successfully").toConsole(); + detailOutput.statusSuccess("Push from #serverConfig.host# completed successfully"); } catch (any e) { failureCount++; - print.redLine("Failed to push from #serverConfig.host#: #e.message#").toConsole(); + detailOutput.statusFailed("Failed to push from #serverConfig.host#: #e.message#"); } } - print.line().toConsole(); - print.boldCyanLine("Push Operations Summary:").toConsole(); - print.greenLine(" Successful: #successCount#").toConsole(); + detailOutput.line(); + detailOutput.output("Push Operations Summary:"); + detailOutput.statusSuccess(" Successful: #successCount#"); if (failureCount > 0) { - print.redLine(" Failed: #failureCount#").toConsole(); + detailOutput.statusFailed(" Failed: #failureCount#"); } } @@ -373,9 +372,9 @@ component extends="DockerCommand" { if (!testSSHConnection(local.host, local.user, local.port)) { error("SSH connection failed to #local.host#"); } - print.greenLine("SSH connection successful").toConsole(); + detailOutput.statusSuccess("SSH connection successful"); - print.cyanLine("Registry: " & arguments.registry).toConsole(); + detailOutput.output("Registry: " & arguments.registry); // Determine final image name local.projectName = getProjectName(); @@ -388,11 +387,11 @@ component extends="DockerCommand" { arguments.namespace ); - print.cyanLine("Target image: " & local.finalImage).toConsole(); + detailOutput.output("Target image: " & local.finalImage); // Tag the image on the server if it's different (e.g. if tagging project name to full name) if (local.finalImage != arguments.image) { - print.yellowLine("Tagging image on server: " & arguments.image & " -> " & local.finalImage).toConsole(); + detailOutput.identical("Tagging image on server: " & arguments.image & " -> " & local.finalImage).toConsole(); local.tagCmd = "docker tag " & arguments.image & " " & local.finalImage; executeRemoteCommand(local.host, local.user, local.port, local.tagCmd); } @@ -415,20 +414,18 @@ component extends="DockerCommand" { // Execute login on remote server if (len(local.loginCmd)) { - print.yellowLine("Logging in to registry on remote server...").toConsole(); + detailOutput.output("Logging in to registry on remote server..."); executeRemoteCommand(local.host, local.user, local.port, local.loginCmd); - print.greenLine("Login successful").toConsole(); + detailOutput.statusSuccess("Login successful"); } else { - print.yellowLine("No password provided, skipping login on remote server...").toConsole(); + detailOutput.skip("No password provided, skipping login on remote server..."); } // Push the image - print.yellowLine("Pushing image from remote server...").toConsole(); + detailOutput.output("Pushing image from remote server..."); local.pushCmd = "docker push " & arguments.image; executeRemoteCommand(local.host, local.user, local.port, local.pushCmd); - - print.boldGreenLine("Image pushed successfully from #local.host#!").toConsole(); + detailOutput.success("Image pushed successfully from #local.host#!"); } - } \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/stop.cfc b/cli/src/commands/wheels/docker/stop.cfc index 85e1767561..c05627bf96 100644 --- a/cli/src/commands/wheels/docker/stop.cfc +++ b/cli/src/commands/wheels/docker/stop.cfc @@ -11,6 +11,8 @@ */ component extends="DockerCommand" { + property name="detailOutput" inject="DetailOutputService@wheels-cli"; + /** * @local Stop containers on local machine * @remote Stop containers on remote server(s) @@ -50,9 +52,7 @@ component extends="DockerCommand" { // ============================================================================= private function stopLocal(boolean removeContainer) { - print.line(); - print.boldMagentaLine("Wheels Docker Local Stop"); - print.line(); + detailOutput.header("Wheels Docker Local Stop"); // Check if Docker is installed locally if (!isDockerInstalled()) { @@ -63,50 +63,45 @@ component extends="DockerCommand" { local.useCompose = hasDockerComposeFile(); if (local.useCompose) { - print.greenLine("Found docker-compose file, will stop docker-compose services").toConsole(); - - print.yellowLine("Stopping services with docker-compose...").toConsole(); + detailOutput.statusSuccess("Found docker-compose file, will stop docker-compose services"); + detailOutput.output("Stopping services with docker-compose..."); try { runLocalCommand(["docker", "compose", "down"]); - print.boldGreenLine("Docker Compose services stopped successfully!").toConsole(); + detailOutput.success("Docker Compose services stopped successfully!"); } catch (any e) { - print.yellowLine("Services might not be running").toConsole(); + detailOutput.statusFailed("Services might not be running"); } } else { - print.greenLine("No docker-compose file found, will use standard docker commands").toConsole(); + detailOutput.output("No docker-compose file found, will use standard docker commands"); // Get project name for container naming local.containerName = getProjectName(); - print.yellowLine("Stopping Docker container '" & local.containerName & "'...").toConsole(); + detailOutput.output("Stopping Docker container '" & local.containerName & "'..."); try { - runLocalCommand(["docker", "stop", local.containerName]); - print.greenLine("Container stopped successfully").toConsole(); + runLocalCommand(["docker", "stop", local.containerName]); + detailOutput.statusSuccess("Container stopped successfully"); if (arguments.removeContainer) { - print.yellowLine("Removing Docker container '" & local.containerName & "'...").toConsole(); + detailOutput.update("Removing Docker container '" & local.containerName & "'..."); runLocalCommand(["docker", "rm", local.containerName]); - print.greenLine("Container removed successfully").toConsole(); + detailOutput.statusSuccess("Container removed successfully").toConsole(); } - print.boldGreenLine("Container operations completed!").toConsole(); + detailOutput.statusSuccess("Container operations completed!"); } catch (any e) { - print.yellowLine("Container might not be running: " & e.message).toConsole(); + detailOutput.output("Container might not be running: " & e.message); } } - print.line(); - print.yellowLine("Check container status with: docker ps -a").toConsole(); - print.line(); + detailOutput.line(); + detailOutput.output("Check container status with: docker ps -a"); + detailOutput.line(); } - - - - // ============================================================================= // REMOTE STOP // ============================================================================= @@ -123,7 +118,7 @@ component extends="DockerCommand" { if (len(trim(arguments.serverNumbers)) == 0 && fileExists(ymlConfigPath)) { var deployConfig = getDeployConfig(); if (arrayLen(deployConfig.servers)) { - print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + detailOutput.identical("Found config/deploy.yml, loading server configuration"); allServers = deployConfig.servers; // Add defaults @@ -144,11 +139,11 @@ component extends="DockerCommand" { if (arrayLen(serversToStop) == 0) { if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.txt, loading server configuration"); allServers = loadServersFromTextFile("deploy-servers.txt"); serversToStop = filterServers(allServers, arguments.serverNumbers); } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + detailOutput.identical("Found deploy-servers.json, loading server configuration"); allServers = loadServersFromConfig("deploy-servers.json"); serversToStop = filterServers(allServers, arguments.serverNumbers); } else { @@ -160,12 +155,14 @@ component extends="DockerCommand" { error("No servers configured to stop containers"); } - print.line().boldCyanLine("Stopping containers on #arrayLen(serversToStop)# server(s)...").toConsole(); + detailOutput.line(); + detailOutput.output("Stopping containers on #arrayLen(serversToStop)# server(s)..."); // Stop containers on all selected servers stopContainersOnServers(serversToStop, arguments.removeContainer); - print.line().boldGreenLine("Container stop operations completed!").toConsole(); + detailOutput.line(); + detailOutput.statusSuccess("Container stop operations completed!"); } /** @@ -185,16 +182,16 @@ component extends="DockerCommand" { if (num > 0 && num <= arrayLen(arguments.allServers)) { arrayAppend(selectedServers, arguments.allServers[num]); } else { - print.yellowLine("Skipping invalid server number: " & numStr).toConsole(); + detailOutput.skip("Skipping invalid server number: " & numStr); } } if (arrayLen(selectedServers) == 0) { - print.yellowLine("No valid servers selected, using all servers").toConsole(); + detailOutput.output("No valid servers selected, using all servers"); return arguments.allServers; } - print.greenLine("Selected #arrayLen(selectedServers)# of #arrayLen(arguments.allServers)# server(s)").toConsole(); + detailOutput.statusSuccess("Selected #arrayLen(selectedServers)# of #arrayLen(arguments.allServers)# server(s)"); return selectedServers; } @@ -208,25 +205,23 @@ component extends="DockerCommand" { for (var i = 1; i <= arrayLen(servers); i++) { serverConfig = servers[i]; - print.line().boldCyanLine("---------------------------------------").toConsole(); - print.boldCyanLine("Stopping container on server #i# of #arrayLen(servers)#: #serverConfig.host#").toConsole(); - print.line().boldCyanLine("---------------------------------------").toConsole(); - + detailOutput.header("Stopping container on server #i# of #arrayLen(servers)#: #serverConfig.host#"); + try { stopContainerOnServer(serverConfig, arguments.removeContainer); successCount++; - print.greenLine("Container on #serverConfig.host# stopped successfully").toConsole(); + detailOutput.statusSuccess("Container on #serverConfig.host# stopped successfully"); } catch (any e) { failureCount++; - print.redLine("Failed to stop container on #serverConfig.host#: #e.message#").toConsole(); + detailOutput.statusFailed("Failed to stop container on #serverConfig.host#: #e.message#"); } } - print.line().toConsole(); - print.boldCyanLine("Stop Operations Summary:").toConsole(); - print.greenLine(" Successful: #successCount#").toConsole(); - if (failureCount > 0) { - print.redLine(" Failed: #failureCount#").toConsole(); + detailOutput.line(); + detailOutput.output("Stop Operations Summary:"); + detailOutput.statusSuccess(" Successful: #successCount#"); + if (failureCount > 0) { + detailOutput.statusFailed(" Failed: #failureCount#"); } } @@ -246,7 +241,7 @@ component extends="DockerCommand" { if (!testSSHConnection(local.host, local.user, local.port)) { error("SSH connection failed to #local.host#. Check credentials and access."); } - print.greenLine("SSH connection successful").toConsole(); + detailOutput.statusSuccess("SSH connection successful"); // Check if docker-compose is being used on the remote server local.checkComposeCmd = "test -f " & local.remoteDir & "/docker-compose.yml || test -f " & local.remoteDir & "/docker-compose.yaml"; @@ -255,14 +250,14 @@ component extends="DockerCommand" { try { executeRemoteCommand(local.host, local.user, local.port, local.checkComposeCmd); local.useCompose = true; - print.greenLine("Found docker-compose file on remote server").toConsole(); + detailOutput.identical("Found docker-compose file on remote server"); } catch (any e) { - print.yellowLine("No docker-compose file found, using standard docker commands").toConsole(); + detailOutput.identical("No docker-compose file found, using standard docker commands"); } if (local.useCompose) { // Stop using docker-compose - print.yellowLine("Stopping services with docker-compose...").toConsole(); + detailOutput.output("Stopping services with docker-compose...").toConsole(); // Check if user can run docker without sudo local.stopCmd = "cd " & local.remoteDir & " && "; @@ -272,13 +267,13 @@ component extends="DockerCommand" { try { executeRemoteCommand(local.host, local.user, local.port, local.stopCmd); - print.greenLine("Docker Compose services stopped").toConsole(); + detailOutput.statusSuccess("Docker Compose services stopped"); } catch (any e) { - print.yellowLine("Services might not be running: " & e.message).toConsole(); + detailOutput.statusWarning("Services might not be running: " & e.message); } } else { // Stop the container using standard docker commands - print.yellowLine("Stopping Docker container '" & local.imageName & "'...").toConsole(); + detailOutput.output("Stopping Docker container '" & local.imageName & "'..."); // Check if user can run docker without sudo local.stopCmd = "if groups | grep -q docker && [ -w /var/run/docker.sock ]; then "; @@ -287,15 +282,16 @@ component extends="DockerCommand" { try { executeRemoteCommand(local.host, local.user, local.port, local.stopCmd); - print.greenLine("Container stopped").toConsole(); + detailOutput.statusSuccess("Container stopped"); } catch (any e) { - print.yellowLine("Container might not be running: " & e.message).toConsole(); + detailOutput.statusWarning("Container might not be running: " & e.message); } // Remove container if requested if (arguments.removeContainer) { - print.yellowLine("Removing Docker container '" & local.imageName & "'...").toConsole(); + detailOutput.output("Removing Docker container '" & local.imageName & "'..."); + // Check if user can run docker without sudo local.removeCmd = "if groups | grep -q docker && [ -w /var/run/docker.sock ]; then "; local.removeCmd &= "docker rm " & local.imageName & "; "; @@ -303,15 +299,14 @@ component extends="DockerCommand" { try { executeRemoteCommand(local.host, local.user, local.port, local.removeCmd); - print.greenLine("Container removed").toConsole(); + detailOutput.statusSuccess("Container removed"); } catch (any e) { - print.yellowLine("Container might not exist: " & e.message).toConsole(); + detailOutput.statusWarning("Container might not exist: " & e.message); } } } - print.boldGreenLine("Operations on #local.host# completed!").toConsole(); + detailOutput.success("Operations on #local.host# completed!"); } - } \ No newline at end of file From 758c7cfc00d509d321014751ad3da37354f1529b Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 23 Jan 2026 13:04:06 +0500 Subject: [PATCH 017/405] commit: update status for wheels cli docker commands --- .../commands/wheels/docker/DockerCommand.cfc | 30 ++++----- cli/src/commands/wheels/docker/build.cfc | 32 ++++----- cli/src/commands/wheels/docker/deploy.cfc | 66 +++++++++---------- cli/src/commands/wheels/docker/exec.cfc | 6 +- cli/src/commands/wheels/docker/logs.cfc | 6 +- cli/src/commands/wheels/docker/push.cfc | 31 +++++---- cli/src/commands/wheels/docker/stop.cfc | 24 +++---- 7 files changed, 94 insertions(+), 101 deletions(-) diff --git a/cli/src/commands/wheels/docker/DockerCommand.cfc b/cli/src/commands/wheels/docker/DockerCommand.cfc index f22dd76aff..18d3b5bbbf 100644 --- a/cli/src/commands/wheels/docker/DockerCommand.cfc +++ b/cli/src/commands/wheels/docker/DockerCommand.cfc @@ -30,14 +30,13 @@ component extends="../base" { switch(lCase(arguments.registry)) { case "dockerhub": if (!len(trim(local.username))) { - detailOutput.output("Enter Docker Hub username:"); - local.username = ask(""); + local.username = ask("Enter Docker Hub username:"); } detailOutput.output("Logging in to Docker Hub..."); if (!len(trim(local.password))) { - detailOutput.output("Enter Docker Hub password or access token:"); + detailOutput.output("Enter Docker Hub password or access token:"); local.password = ask(message="", mask="*"); } @@ -52,7 +51,7 @@ component extends="../base" { case "ecr": detailOutput.output("Logging in to AWS ECR..."); - detailOutput.invoke("Note: AWS CLI must be configured with valid credentials"); + detailOutput.output("Note: AWS CLI must be configured with valid credentials"); // Extract region from image name if (!len(trim(arguments.image))) { @@ -87,7 +86,7 @@ component extends="../base" { break; case "gcr": - detailOutput.create("Logging in to Google Container Registry..."); + detailOutput.statusInfo("Logging in to Google Container Registry..."); local.keyFile = ""; if (fileExists(getCWD() & "/gcr-key.json")) { @@ -139,8 +138,7 @@ component extends="../base" { // 2. Resolve Username if (!len(trim(local.username))) { - detailOutput.output("Enter Azure ACR username:"); - local.username = ask(""); + local.username = ask("Enter Azure ACR username:"); } detailOutput.create("Logging in to Azure Container Registry: #local.registryUrl#"); @@ -162,10 +160,9 @@ component extends="../base" { case "ghcr": if (!len(trim(local.username))) { - detailOutput.output("Enter GitHub username:"); - local.username = ask(""); + local.username = ask("Enter GitHub username:"); } - detailOutput.create("Logging in to GitHub Container Registry..."); + detailOutput.statusInfo("Logging in to GitHub Container Registry..."); if (!len(trim(local.password))) { detailOutput.output("Enter Personal Access Token (PAT) with write:packages scope:"); @@ -202,11 +199,10 @@ component extends="../base" { // 2. Resolve Username if (!len(trim(local.username))) { - detailOutput.output("Enter registry username:"); - local.username = ask(""); + local.username = ask("Enter registry username:"); } - detailOutput.create("Logging in to private registry: #local.registryUrl#"); + detailOutput.statusInfo("Logging in to private registry: #local.registryUrl#"); // 3. Resolve Password if (!len(trim(local.password))) { @@ -421,8 +417,7 @@ component extends="../base" { if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image))) { local.registryUrl = listFirst(deployConfig.image, "/"); } else { - detailOutput.output("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); - local.registryUrl = ask(""); + local.registryUrl = ask("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); if (!len(trim(local.registryUrl))) { error("Azure ACR requires a registry URL to determine image path."); } @@ -442,8 +437,7 @@ component extends="../base" { if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image))) { local.registryUrl = listFirst(deployConfig.image, "/"); } else { - detailOutput.output("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); - local.registryUrl = ask(""); + local.registryUrl = ask("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); if (!len(trim(local.registryUrl))) { error("Private registry requires a registry URL to determine image path."); } @@ -629,7 +623,7 @@ component extends="../base" { public function testSSHConnection(string host, string user, numeric port) { var local = {}; - detailOutput.output("Testing SSH connection to " & arguments.host & "..."); + detailOutput.statusInfo("Testing SSH connection to " & arguments.host & "..."); var sshCmd = ["ssh", "-p", arguments.port]; sshCmd.addAll(getSSHOptions()); sshCmd.addAll([arguments.user & "@" & arguments.host, "echo connected"]); diff --git a/cli/src/commands/wheels/docker/build.cfc b/cli/src/commands/wheels/docker/build.cfc index bcc4ab3969..55909dd702 100644 --- a/cli/src/commands/wheels/docker/build.cfc +++ b/cli/src/commands/wheels/docker/build.cfc @@ -248,13 +248,13 @@ component extends="DockerCommand" { } detailOutput.line(); - detailOutput.create("Building Docker images on #arrayLen(serversToBuild)# server(s)..."); + detailOutput.statusInfo("Building Docker images on #arrayLen(serversToBuild)# server(s)..."); // Build on all selected servers buildOnServers(serversToBuild, arguments.customTag, arguments.nocache, arguments.pull); detailOutput.line(); - detailOutput.statusSuccess("Build operations completed on all servers!"); + detailOutput.success("Build operations completed on all servers!"); } /** @@ -310,7 +310,7 @@ component extends="DockerCommand" { } detailOutput.line(); - detailOutput.output("Build Operations Summary:"); + detailOutput.statusInfo("Build Operations Summary:"); detailOutput.statusSuccess(" Successful: #successCount#"); if (failureCount > 0) { detailOutput.statusFailed(" Failed: #failureCount#"); @@ -335,7 +335,7 @@ component extends="DockerCommand" { detailOutput.statusSuccess("SSH connection successful"); // Check if remote directory exists - detailOutput.output("Checking remote directory..."); + detailOutput.statusInfo("Checking remote directory..."); local.checkDirCmd = "test -d " & local.remoteDir; local.dirExists = false; @@ -344,7 +344,7 @@ component extends="DockerCommand" { local.dirExists = true; detailOutput.statusSuccess("Remote directory exists"); } catch (any e) { - detailOutput.output("Remote directory does not exist, uploading source code..."); + detailOutput.statusInfo("Remote directory does not exist, uploading source code..."); uploadSourceCode(local.host, local.user, local.port, local.remoteDir); } @@ -357,7 +357,7 @@ component extends="DockerCommand" { local.useCompose = true; detailOutput.statusSuccess("Found docker-compose file on remote server"); } catch (any e) { - detailOutput.output("No docker-compose file found, checking for Dockerfile..."); + detailOutput.statusInfo("No docker-compose file found, checking for Dockerfile..."); // Check if Dockerfile exists local.checkDockerfileCmd = "test -f " & local.remoteDir & "/Dockerfile"; @@ -371,7 +371,7 @@ component extends="DockerCommand" { if (local.useCompose) { // Build using docker-compose - detailOutput.output("Building with docker-compose..."); + detailOutput.statusInfo("Building with docker-compose..."); local.buildCmd = "cd " & local.remoteDir & " && "; @@ -404,7 +404,7 @@ component extends="DockerCommand" { } else { // Build using standard docker build - detailOutput.create("Building Docker image..."); + detailOutput.statusInfo("Building Docker image..."); // Determine tag local.projectName = getProjectName(); @@ -451,7 +451,7 @@ component extends="DockerCommand" { local.buildCmd &= "; fi"; executeRemoteCommand(local.host, local.user, local.port, local.buildCmd); - detailOutput.statusSuccess("Docker image built: " & local.imageTag); + detailOutput.create("Docker image built: " & local.imageTag); } detailOutput.success("Build operations on #local.host# completed!"); @@ -463,7 +463,7 @@ component extends="DockerCommand" { private function uploadSourceCode(string host, string user, numeric port, string remoteDir) { var local = {}; - detailOutput.create("Creating deployment directory on remote server..."); + detailOutput.statusInfo("Creating deployment directory on remote server..."); // Create remote directory local.createDirCmd = "sudo mkdir -p " & arguments.remoteDir & " && sudo chown -R $USER:$USER " & arguments.remoteDir; @@ -471,7 +471,7 @@ component extends="DockerCommand" { try { executeRemoteCommand(arguments.host, arguments.user, arguments.port, local.createDirCmd); } catch (any e) { - detailOutput.output("Note: Creating directory without sudo..."); + detailOutput.statusInfo("Note: Creating directory without sudo..."); executeRemoteCommand(arguments.host, arguments.user, arguments.port, "mkdir -p " & arguments.remoteDir); } @@ -480,14 +480,14 @@ component extends="DockerCommand" { local.tarFile = getTempFile(getTempDirectory(), "buildsrc_") & ".tar.gz"; local.remoteTar = "/tmp/buildsrc_" & local.timestamp & ".tar.gz"; - detailOutput.create("Creating source tarball..."); + detailOutput.statusInfo("Creating source tarball..."); runProcess(["tar", "-czf", local.tarFile, "-C", getCWD(), "."]); - detailOutput.create("Uploading source code to remote server..."); + detailOutput.statusInfo("Uploading source code to remote server..."); runProcess(["scp", "-P", arguments.port, local.tarFile, arguments.user & "@" & arguments.host & ":" & local.remoteTar]); fileDelete(local.tarFile); - detailOutput.create("Extracting source code..."); + detailOutput.statusInfo("Extracting source code..."); local.extractCmd = "tar -xzf " & local.remoteTar & " -C " & arguments.remoteDir & " && rm " & local.remoteTar; executeRemoteCommand(arguments.host, arguments.user, arguments.port, local.extractCmd); @@ -587,7 +587,7 @@ component extends="DockerCommand" { private function testSSHConnection(string host, string user, numeric port) { var local = {}; - detailOutput.output("Testing SSH connection to " & arguments.host & "..."); + detailOutput.statusInfo("Testing SSH connection to " & arguments.host & "..."); local.result = runProcess([ "ssh", "-o", "BatchMode=yes", @@ -603,7 +603,7 @@ component extends="DockerCommand" { private function executeRemoteCommand(string host, string user, numeric port, string cmd) { var local = {}; - detailOutput.output("Running: ssh -p " & arguments.port & " " & arguments.user & "@" & arguments.host & " " & arguments.cmd); + detailOutput.statusInfo("Running: ssh -p " & arguments.port & " " & arguments.user & "@" & arguments.host & " " & arguments.cmd); local.result = runProcess([ "ssh", diff --git a/cli/src/commands/wheels/docker/deploy.cfc b/cli/src/commands/wheels/docker/deploy.cfc index 06150538b6..cfa9f2258b 100644 --- a/cli/src/commands/wheels/docker/deploy.cfc +++ b/cli/src/commands/wheels/docker/deploy.cfc @@ -98,7 +98,7 @@ component extends="DockerCommand" { detailOutput.statusSuccess("Using custom tag: " & arguments.tag); } else { // Empty selection matches 'latest' default logic later, or we can explicit set it - detailOutput.output("No selection made, defaulting to 'latest'"); + detailOutput.statusInfo("No selection made, defaulting to 'latest'"); } } } @@ -147,10 +147,10 @@ component extends="DockerCommand" { // Just run docker-compose up if (len(arguments.tag)) { - detailOutput.output("Note: --tag argument is ignored when using docker-compose."); + detailOutput.statusInfo("Note: --tag argument is ignored when using docker-compose."); } - detailOutput.output("Starting services..."); + detailOutput.statusInfo("Starting services..."); runLocalCommand(["docker-compose", "up", "-d", "--build"]); detailOutput.line(); @@ -176,7 +176,7 @@ component extends="DockerCommand" { // Extract port from Dockerfile local.exposedPort = getDockerExposedPort(); if (!len(local.exposedPort)) { - detailOutput.output("No EXPOSE directive found in Dockerfile, using default port 8080"); + detailOutput.statusInfo("No EXPOSE directive found in Dockerfile, using default port 8080"); local.exposedPort = "8080"; } else { detailOutput.statusSuccess("Found EXPOSE port: " & local.exposedPort); @@ -201,10 +201,10 @@ component extends="DockerCommand" { // Container Name: Always use project name for consistency local.containerName = local.projectName; - detailOutput.create("Building Docker image (" & local.imageName & ")..."); + detailOutput.statusInfo("Building Docker image (" & local.imageName & ")..."); runLocalCommand(["docker", "build", "-t", local.imageName, "."]); - detailOutput.output("Starting container..."); + detailOutput.statusInfo("Starting container..."); try { // Stop and remove existing container @@ -220,9 +220,9 @@ component extends="DockerCommand" { detailOutput.line(); detailOutput.statusSuccess("Container started successfully!"); detailOutput.line(); - detailOutput.output("Image: " & local.imageName); - detailOutput.output("Container: " & local.containerName); - detailOutput.output("Access your application at: http://localhost:" & local.exposedPort); + detailOutput.create("Image: " & local.imageName); + detailOutput.create("Container: " & local.containerName); + detailOutput.statusInfo("Access your application at: http://localhost:" & local.exposedPort); detailOutput.line(); detailOutput.output("Check container status with: docker ps"); detailOutput.output("View logs with: wheels docker logs --local"); @@ -300,7 +300,7 @@ component extends="DockerCommand" { error("No servers configured for deployment"); } - detailOutput.identical("Starting remote deployment to #arrayLen(servers)# server(s)..."); + detailOutput.statusInfo("Starting remote deployment to #arrayLen(servers)# server(s)..."); if (arguments.blueGreen) { detailOutput.output("Strategy: Blue/Green Deployment (Zero Downtime)"); } @@ -384,11 +384,11 @@ component extends="DockerCommand" { if (!arguments.skipDockerCheck) { ensureDockerInstalled(local.host, local.user, local.port); } else { - detailOutput.output("Skipping Docker installation check (--skipDockerCheck flag is set)"); + detailOutput.skip("Skipping Docker installation check (--skipDockerCheck flag is set)"); } // Step 2: Create remote directory - detailOutput.create("Creating remote directory..."); + detailOutput.statusInfo("Creating remote directory..."); executeRemoteCommand(local.host, local.user, local.port, "mkdir -p " & local.remoteDir); // Step 3: Check for docker-compose file @@ -399,7 +399,7 @@ component extends="DockerCommand" { // Extract port from Dockerfile for standard docker run local.exposedPort = getDockerExposedPort(); if (!len(local.exposedPort)) { - detailOutput.output(" No EXPOSE directive found in Dockerfile, using default port 8080"); + detailOutput.statusInfo(" No EXPOSE directive found in Dockerfile, using default port 8080"); local.exposedPort = "8080"; } else { detailOutput.statusSuccess("Found EXPOSE port: " & local.exposedPort); @@ -411,10 +411,10 @@ component extends="DockerCommand" { local.tarFile = getTempFile(getTempDirectory(), "deploysrc_") & ".tar.gz"; local.remoteTar = "/tmp/deploysrc_" & local.timestamp & ".tar.gz"; - detailOutput.create("Creating source tarball..."); + detailOutput.statusInfo("Creating source tarball..."); runLocalCommand(["tar", "-czf", local.tarFile, "-C", getCWD(), "."]); - detailOutput.output(" Uploading to remote server..."); + detailOutput.statusInfo(" Uploading to remote server..."); var scpCmd = ["scp", "-P", local.port]; scpCmd.addAll(getSSHOptions()); scpCmd.addAll([local.tarFile, local.user & "@" & local.host & ":" & local.remoteTar]); @@ -468,14 +468,14 @@ component extends="DockerCommand" { local.tempFile = getTempFile(getTempDirectory(), "deploy_"); fileWrite(local.tempFile, local.deployScript); - detailOutput.output("Uploading deployment script..."); + detailOutput.statusInfo("Uploading deployment script..."); var scpScriptCmd = ["scp", "-P", local.port]; scpScriptCmd.addAll(getSSHOptions()); scpScriptCmd.addAll([local.tempFile, local.user & "@" & local.host & ":/tmp/deploy-simple.sh"]); runLocalCommand(scpScriptCmd); fileDelete(local.tempFile); - detailOutput.output("Executing deployment script remotely..."); + detailOutput.statusInfo("Executing deployment script remotely..."); // Use interactive command var execCmd = ["ssh", "-p", local.port]; execCmd.addAll(getSSHOptions()); @@ -512,13 +512,13 @@ component extends="DockerCommand" { } // Step 2: Create remote directory - detailOutput.create("Creating remote directory..."); + detailOutput.statusInfo("Creating remote directory..."); executeRemoteCommand(local.host, local.user, local.port, "mkdir -p " & local.remoteDir); // Step 3: Determine Port local.exposedPort = getDockerExposedPort(); if (!len(local.exposedPort)) { - detailOutput.output(" No EXPOSE directive found in Dockerfile, using default port 8080"); + detailOutput.statusInfo(" No EXPOSE directive found in Dockerfile, using default port 8080"); local.exposedPort = "8080"; } else { detailOutput.statusSuccess("Found EXPOSE port: " & local.exposedPort); @@ -529,10 +529,10 @@ component extends="DockerCommand" { local.tarFile = getTempFile(getTempDirectory(), "deploysrc_") & ".tar.gz"; local.remoteTar = "/tmp/deploysrc_" & local.timestamp & ".tar.gz"; - detailOutput.create("Creating source tarball..."); + detailOutput.statusInfo("Creating source tarball..."); runLocalCommand(["tar", "-czf", local.tarFile, "-C", getCWD(), "."]); - detailOutput.output(" Uploading to remote server..."); + detailOutput.statusInfo(" Uploading to remote server..."); var scpCmd = ["scp", "-P", local.port]; scpCmd.addAll(getSSHOptions()); scpCmd.addAll([local.tarFile, local.user & "@" & local.host & ":" & local.remoteTar]); @@ -624,14 +624,14 @@ component extends="DockerCommand" { local.tempFile = getTempFile(getTempDirectory(), "deploy_bg_"); fileWrite(local.tempFile, local.deployScript); - detailOutput.output("Uploading Blue/Green deployment script..."); + detailOutput.statusInfo("Uploading Blue/Green deployment script..."); var scpScriptCmd = ["scp", "-P", local.port]; scpScriptCmd.addAll(getSSHOptions()); scpScriptCmd.addAll([local.tempFile, local.user & "@" & local.host & ":/tmp/deploy-bluegreen.sh"]); runLocalCommand(scpScriptCmd); fileDelete(local.tempFile); - detailOutput.output("Executing Blue/Green deployment script remotely..."); + detailOutput.statusInfo("Executing Blue/Green deployment script remotely..."); var execCmd = ["ssh", "-p", local.port]; execCmd.addAll(getSSHOptions()); execCmd.addAll([local.user & "@" & local.host, "chmod +x /tmp/deploy-bluegreen.sh && bash /tmp/deploy-bluegreen.sh"]); @@ -647,7 +647,7 @@ component extends="DockerCommand" { private function ensureDockerInstalled(string host, string user, numeric port) { var local = {}; - detailOutput.output("Checking Docker installation on remote server..."); + detailOutput.statusInfo("Checking Docker installation on remote server..."); // Check if Docker is installed var checkCmd = ["ssh", "-p", arguments.port]; @@ -667,7 +667,7 @@ component extends="DockerCommand" { local.versionResult = runLocalCommand(versionCmd); if (local.versionResult.exitCode eq 0) { - detailOutput.output("Docker version: " & trim(local.versionResult.output)); + detailOutput.statusInfo("Docker version: " & trim(local.versionResult.output)); } // Check if docker compose is available @@ -676,10 +676,10 @@ component extends="DockerCommand" { return true; } - detailOutput.output("Docker is not installed. Attempting to install Docker..."); + detailOutput.statusInfo("Docker is not installed. Attempting to install Docker..."); // Check if user has passwordless sudo access - detailOutput.output("Checking sudo access..."); + detailOutput.statusInfo("Checking sudo access..."); var sudoCheckCmd = ["ssh", "-p", arguments.port]; sudoCheckCmd.addAll(getSSHOptions()); sudoCheckCmd.addAll([arguments.user & "@" & arguments.host, "sudo -n true 2>&1"]); @@ -710,10 +710,10 @@ component extends="DockerCommand" { if (findNoCase("ubuntu", local.osResult.output) || findNoCase("debian", local.osResult.output)) { local.installScript = getDockerInstallScriptDebian(); - detailOutput.skip("Detected Debian/Ubuntu system"); + detailOutput.identical("Detected Debian/Ubuntu system"); } else if (findNoCase("centos", local.osResult.output) || findNoCase("rhel", local.osResult.output) || findNoCase("fedora", local.osResult.output)) { local.installScript = getDockerInstallScriptRHEL(); - detailOutput.skip("Detected RHEL/CentOS/Fedora system"); + detailOutput.identical("Detected RHEL/CentOS/Fedora system"); } else { error("Unsupported OS. Docker installation is only automated for Ubuntu/Debian and RHEL/CentOS/Fedora systems."); } @@ -728,7 +728,7 @@ component extends="DockerCommand" { fileWrite(local.tempFile, local.installScript); // Upload install script - detailOutput.output("Uploading Docker installation script..."); + detailOutput.statusInfo("Uploading Docker installation script..."); var scpInstallCmd = ["scp", "-P", arguments.port]; scpInstallCmd.addAll(getSSHOptions()); scpInstallCmd.addAll([local.tempFile, arguments.user & "@" & arguments.host & ":/tmp/install-docker.sh"]); @@ -736,7 +736,7 @@ component extends="DockerCommand" { fileDelete(local.tempFile); // Execute install script - detailOutput.output("Installing Docker..."); + detailOutput.statusInfo("Installing Docker..."); var installCmd = ["ssh", "-p", arguments.port]; installCmd.addAll(getSSHOptions()); installCmd.addAll(["-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=10"]); @@ -779,7 +779,7 @@ component extends="DockerCommand" { if (local.composeResult.exitCode eq 0) { detailOutput.statusSuccess("Docker Compose is available"); - detailOutput.output("Compose version: " & trim(local.composeResult.output)); + detailOutput.statusInfo("Compose version: " & trim(local.composeResult.output)); return true; } @@ -792,7 +792,7 @@ component extends="DockerCommand" { if (local.oldComposeResult.exitCode eq 0) { detailOutput.statusSuccess("Docker Compose (standalone) is available"); - detailOutput.output("Compose version: " & trim(local.oldComposeResult.output)); + detailOutput.statusInfo("Compose version: " & trim(local.oldComposeResult.output)); return true; } diff --git a/cli/src/commands/wheels/docker/exec.cfc b/cli/src/commands/wheels/docker/exec.cfc index 53325a83f3..d68406c11c 100644 --- a/cli/src/commands/wheels/docker/exec.cfc +++ b/cli/src/commands/wheels/docker/exec.cfc @@ -115,7 +115,7 @@ component extends="DockerCommand" { // Check for UserInterruptException (CommandBox specific) or standard InterruptedException if (findNoCase("UserInterruptException", e.message) || findNoCase("InterruptedException", e.message) || (structKeyExists(e, "type") && findNoCase("UserInterruptException", e.type))) { detailOutput.line(); - detailOutput.statusFailed("Command interrupted by user."); + detailOutput.error("Command interrupted by user."); break; } detailOutput.statusFailed("Failed to execute command on #serverConfig.host#: #e.message#"); @@ -213,8 +213,8 @@ component extends="DockerCommand" { execCmd.addAll([local.user & "@" & local.host, dockerCmd]); // 4. Execute - detailOutput.output("Executing: " & arguments.command); - detailOutput.output("Container: " & containerName); + detailOutput.statusInfo("Executing: " & arguments.command); + detailOutput.statusInfo("Container: " & containerName); detailOutput.output(); // Use runInteractiveCommand for both interactive and non-interactive diff --git a/cli/src/commands/wheels/docker/logs.cfc b/cli/src/commands/wheels/docker/logs.cfc index abd796dcac..5b7e9baae8 100644 --- a/cli/src/commands/wheels/docker/logs.cfc +++ b/cli/src/commands/wheels/docker/logs.cfc @@ -126,7 +126,7 @@ component extends="DockerCommand" { // Check for UserInterruptException (CommandBox specific) or standard InterruptedException if (findNoCase("UserInterruptException", e.message) || findNoCase("InterruptedException", e.message) || (structKeyExists(e, "type") && findNoCase("UserInterruptException", e.type))) { detailOutput.line(); - detailOutput.statusFailed("Command interrupted by user."); + detailOutput.error("Command interrupted by user."); break; } detailOutput.statusFailed("Failed to fetch logs from #serverConfig.host#: #e.message#"); @@ -236,7 +236,7 @@ component extends="DockerCommand" { detailOutput.statusInfo("Fetching logs from container: " & containerName); if (arguments.follow) { - detailOutput.output("Following logs... (Press Ctrl+C to stop)"); + detailOutput.statusInfo("Following logs... (Press Ctrl+C to stop)"); } detailOutput.line("----------------------------------------"); @@ -303,7 +303,7 @@ component extends="DockerCommand" { detailOutput.statusInfo("Fetching logs from local container: " & containerName); if (arguments.follow) { - detailOutput.output("Following logs... (Press Ctrl+C to stop)"); + detailOutput.statusInfo("Following logs... (Press Ctrl+C to stop)"); } detailOutput.output("----------------------------------------"); diff --git a/cli/src/commands/wheels/docker/push.cfc b/cli/src/commands/wheels/docker/push.cfc index 0935004ef2..5162ac45d3 100644 --- a/cli/src/commands/wheels/docker/push.cfc +++ b/cli/src/commands/wheels/docker/push.cfc @@ -148,15 +148,14 @@ component extends="DockerCommand" { // Build image if requested if (arguments.build) { - detailOutput.output("Building image before push..."); + detailOutput.statusInfo("Building image before push..."); buildLocalImage(); } // Check if local image exists if (!checkLocalImageExists(local.localImageName)) { - detailOutput.output("Local image '#local.localImageName#' not found."); - detailOutput.output("Would you like to build it now? (y/n)"); - local.answer = ask(""); + detailOutput.statusInfo("Local image '#local.localImageName#' not found."); + local.answer = ask("Would you like to build it now? (y/n)"); if (lCase(local.answer) == "y") { buildLocalImage(); } else { @@ -176,12 +175,12 @@ component extends="DockerCommand" { arguments.namespace ); - detailOutput.output("Target image: " & local.finalImage); + detailOutput.statusInfo("Target image: " & local.finalImage); detailOutput.line(); // Tag the image if needed if (local.finalImage != local.localImageName) { - detailOutput.output("Tagging image: " & local.localImageName & " -> " & local.finalImage); + detailOutput.statusInfo("Tagging image: " & local.localImageName & " -> " & local.finalImage); try { runLocalCommand(["docker", "tag", local.localImageName, local.finalImage]); detailOutput.statusSuccess("Image tagged successfully"); @@ -204,21 +203,21 @@ component extends="DockerCommand" { } // Push the image - detailOutput.output("Pushing image to " & arguments.registry & "..."); + detailOutput.statusInfo("Pushing image to " & arguments.registry & "..."); try { runLocalCommand(["docker", "push", local.finalImage]); detailOutput.line(); detailOutput.statusSuccess("Image pushed successfully to " & arguments.registry & "!"); detailOutput.line(); - detailOutput.output("Image: " & local.finalImage); - detailOutput.create("Pull with: docker pull " & local.finalImage); + detailOutput.statusInfo("Image: " & local.finalImage); + detailOutput.statusInfo("Pull with: docker pull " & local.finalImage); detailOutput.line(); } catch (any e) { detailOutput.statusFailed("Failed to push image: " & e.message); detailOutput.line(); detailOutput.output("You may need to login first."); - detailOutput.invoke("Try running: wheels docker login --registry=" & arguments.registry & " --username=" & arguments.username); + detailOutput.output("Try running: wheels docker login --registry=" & arguments.registry & " --username=" & arguments.username); detailOutput.output("Or provide a password/token with --password"); error("Push failed"); } @@ -228,7 +227,7 @@ component extends="DockerCommand" { * Build the local image */ private function buildLocalImage() { - detailOutput.create("Building Docker image..."); + detailOutput.statusInfo("Building Docker image..."); detailOutput.line(); // Check for docker-compose file @@ -374,7 +373,7 @@ component extends="DockerCommand" { } detailOutput.statusSuccess("SSH connection successful"); - detailOutput.output("Registry: " & arguments.registry); + detailOutput.statusInfo("Registry: " & arguments.registry); // Determine final image name local.projectName = getProjectName(); @@ -387,11 +386,11 @@ component extends="DockerCommand" { arguments.namespace ); - detailOutput.output("Target image: " & local.finalImage); + detailOutput.statusInfo("Target image: " & local.finalImage); // Tag the image on the server if it's different (e.g. if tagging project name to full name) if (local.finalImage != arguments.image) { - detailOutput.identical("Tagging image on server: " & arguments.image & " -> " & local.finalImage).toConsole(); + detailOutput.statusInfo("Tagging image on server: " & arguments.image & " -> " & local.finalImage).toConsole(); local.tagCmd = "docker tag " & arguments.image & " " & local.finalImage; executeRemoteCommand(local.host, local.user, local.port, local.tagCmd); } @@ -414,7 +413,7 @@ component extends="DockerCommand" { // Execute login on remote server if (len(local.loginCmd)) { - detailOutput.output("Logging in to registry on remote server..."); + detailOutput.statusInfo("Logging in to registry on remote server..."); executeRemoteCommand(local.host, local.user, local.port, local.loginCmd); detailOutput.statusSuccess("Login successful"); } else { @@ -422,7 +421,7 @@ component extends="DockerCommand" { } // Push the image - detailOutput.output("Pushing image from remote server..."); + detailOutput.statusInfo("Pushing image from remote server..."); local.pushCmd = "docker push " & arguments.image; executeRemoteCommand(local.host, local.user, local.port, local.pushCmd); detailOutput.success("Image pushed successfully from #local.host#!"); diff --git a/cli/src/commands/wheels/docker/stop.cfc b/cli/src/commands/wheels/docker/stop.cfc index c05627bf96..8b00beb2a2 100644 --- a/cli/src/commands/wheels/docker/stop.cfc +++ b/cli/src/commands/wheels/docker/stop.cfc @@ -64,7 +64,7 @@ component extends="DockerCommand" { if (local.useCompose) { detailOutput.statusSuccess("Found docker-compose file, will stop docker-compose services"); - detailOutput.output("Stopping services with docker-compose..."); + detailOutput.statusInfo("Stopping services with docker-compose..."); try { runLocalCommand(["docker", "compose", "down"]); @@ -74,26 +74,26 @@ component extends="DockerCommand" { } } else { - detailOutput.output("No docker-compose file found, will use standard docker commands"); + detailOutput.statusInfo("No docker-compose file found, will use standard docker commands"); // Get project name for container naming local.containerName = getProjectName(); - detailOutput.output("Stopping Docker container '" & local.containerName & "'..."); + detailOutput.statusInfo("Stopping Docker container '" & local.containerName & "'..."); try { runLocalCommand(["docker", "stop", local.containerName]); detailOutput.statusSuccess("Container stopped successfully"); if (arguments.removeContainer) { - detailOutput.update("Removing Docker container '" & local.containerName & "'..."); + detailOutput.statusInfo("Removing Docker container '" & local.containerName & "'..."); runLocalCommand(["docker", "rm", local.containerName]); detailOutput.statusSuccess("Container removed successfully").toConsole(); } detailOutput.statusSuccess("Container operations completed!"); } catch (any e) { - detailOutput.output("Container might not be running: " & e.message); + detailOutput.statusFailed("Container might not be running: " & e.message); } } @@ -156,7 +156,7 @@ component extends="DockerCommand" { } detailOutput.line(); - detailOutput.output("Stopping containers on #arrayLen(serversToStop)# server(s)..."); + detailOutput.statusInfo("Stopping containers on #arrayLen(serversToStop)# server(s)..."); // Stop containers on all selected servers stopContainersOnServers(serversToStop, arguments.removeContainer); @@ -218,7 +218,7 @@ component extends="DockerCommand" { } detailOutput.line(); - detailOutput.output("Stop Operations Summary:"); + detailOutput.statusInfo("Stop Operations Summary:"); detailOutput.statusSuccess(" Successful: #successCount#"); if (failureCount > 0) { detailOutput.statusFailed(" Failed: #failureCount#"); @@ -250,14 +250,14 @@ component extends="DockerCommand" { try { executeRemoteCommand(local.host, local.user, local.port, local.checkComposeCmd); local.useCompose = true; - detailOutput.identical("Found docker-compose file on remote server"); + detailOutput.statusInfo("Found docker-compose file on remote server"); } catch (any e) { - detailOutput.identical("No docker-compose file found, using standard docker commands"); + detailOutput.statusInfo("No docker-compose file found, using standard docker commands"); } if (local.useCompose) { // Stop using docker-compose - detailOutput.output("Stopping services with docker-compose...").toConsole(); + detailOutput.statusInfo("Stopping services with docker-compose...").toConsole(); // Check if user can run docker without sudo local.stopCmd = "cd " & local.remoteDir & " && "; @@ -273,7 +273,7 @@ component extends="DockerCommand" { } } else { // Stop the container using standard docker commands - detailOutput.output("Stopping Docker container '" & local.imageName & "'..."); + detailOutput.statusInfo("Stopping Docker container '" & local.imageName & "'..."); // Check if user can run docker without sudo local.stopCmd = "if groups | grep -q docker && [ -w /var/run/docker.sock ]; then "; @@ -290,7 +290,7 @@ component extends="DockerCommand" { // Remove container if requested if (arguments.removeContainer) { - detailOutput.output("Removing Docker container '" & local.imageName & "'..."); + detailOutput.statusInfo("Removing Docker container '" & local.imageName & "'..."); // Check if user can run docker without sudo local.removeCmd = "if groups | grep -q docker && [ -w /var/run/docker.sock ]; then "; From 96ec969fa2f7511d2c089ac421cc6c26673eec17 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 23 Jan 2026 13:09:45 +0500 Subject: [PATCH 018/405] commit: Remove remaining print.line occurrences from Docker commands --- cli/src/commands/wheels/docker/build.cfc | 2 +- cli/src/commands/wheels/docker/init.cfc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/commands/wheels/docker/build.cfc b/cli/src/commands/wheels/docker/build.cfc index 55909dd702..65f70c8719 100644 --- a/cli/src/commands/wheels/docker/build.cfc +++ b/cli/src/commands/wheels/docker/build.cfc @@ -643,7 +643,7 @@ component extends="DockerCommand" { local.line = local.br.readLine(); if (isNull(local.line)) break; arrayAppend(local.outputParts, local.line); - print.line(local.line).toConsole(); + detailOutput.output(local.line); } local.exitCode = local.proc.waitFor(); diff --git a/cli/src/commands/wheels/docker/init.cfc b/cli/src/commands/wheels/docker/init.cfc index 59b3266ad7..7a892782d0 100644 --- a/cli/src/commands/wheels/docker/init.cfc +++ b/cli/src/commands/wheels/docker/init.cfc @@ -56,7 +56,7 @@ component extends="DockerCommand" { local.imageName = local.appName; } - print.line().boldCyanLine("Production Server Configuration").toConsole(); + detailOutput.subHeader("Production Server Configuration"); local.serverHost = ask("Server Host/IP (e.g. 192.168.1.10): "); local.serverUser = ""; @@ -66,7 +66,7 @@ component extends="DockerCommand" { local.serverUser = "ubuntu"; } } - print.line().toConsole(); + detailOutput.line(); // Check for existing files if force is not set if (!arguments.force) { From 6b7954756ffa97448f3dadda8e877a0d6e90937d Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Mon, 26 Jan 2026 17:48:56 +0500 Subject: [PATCH 019/405] allowExplicitTimestamps fix Passed the extra argument of allowExplicitTimestamps to $update and $create functions and used the arguments scope instead of this scope. --- core/src/wheels/model/create.cfc | 6 +++--- core/src/wheels/model/update.cfc | 2 +- docs/api/v3.0.0.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/wheels/model/create.cfc b/core/src/wheels/model/create.cfc index 05b97a832d..3be80d9307 100644 --- a/core/src/wheels/model/create.cfc +++ b/core/src/wheels/model/create.cfc @@ -172,7 +172,7 @@ component { if (!Len(key())) { local.rollback = true; } - $create(parameterize = arguments.parameterize, reload = arguments.reload); + $create(parameterize = arguments.parameterize, reload = arguments.reload, allowExplicitTimestamps = arguments.allowExplicitTimestamps?:false); if ( $saveAssociations(argumentCollection = arguments) && $callback("afterCreate", arguments.callbacks) @@ -198,7 +198,7 @@ component { && $callback("beforeSave", arguments.callbacks) && $callback("beforeUpdate", arguments.callbacks) ) { - $update(parameterize = arguments.parameterize, reload = arguments.reload); + $update(parameterize = arguments.parameterize, reload = arguments.reload, allowExplicitTimestamps = arguments.allowExplicitTimestamps?:false); if ( $saveAssociations(argumentCollection = arguments) && $callback("afterUpdate", arguments.callbacks) @@ -225,7 +225,7 @@ component { */ public boolean function $create(required any parameterize, required boolean reload) { // Allow explicit assignment of the createdAt/updatedAt properties if allowExplicitTimestamps is true - local.allowExplicitTimestamps = StructKeyExists(this, "allowExplicitTimestamps") && this.allowExplicitTimestamps; + local.allowExplicitTimestamps = StructKeyExists(arguments, "allowExplicitTimestamps") && arguments.allowExplicitTimestamps; if ( local.allowExplicitTimestamps && StructKeyExists(this, $get("timeStampOnCreateProperty")) diff --git a/core/src/wheels/model/update.cfc b/core/src/wheels/model/update.cfc index 62b5801489..8a228ed478 100644 --- a/core/src/wheels/model/update.cfc +++ b/core/src/wheels/model/update.cfc @@ -304,7 +304,7 @@ component { // Perform update if changes have been made. if (hasChanged()) { // Allow explicit assignment of the createdAt/updatedAt properties if allowExplicitTimestamps is true - local.allowExplicitTimestamps = StructKeyExists(this, "allowExplicitTimestamps") && this.allowExplicitTimestamps; + local.allowExplicitTimestamps = StructKeyExists(arguments, "allowExplicitTimestamps") && arguments.allowExplicitTimestamps; if ( local.allowExplicitTimestamps && StructKeyExists(this, $get("timeStampOnUpdateProperty")) diff --git a/docs/api/v3.0.0.json b/docs/api/v3.0.0.json index c40721f89b..430e4b8656 100644 --- a/docs/api/v3.0.0.json +++ b/docs/api/v3.0.0.json @@ -1 +1 @@ -{"sections":[{"categories":["Miscellaneous Functions","Routing"],"name":"Configuration"},{"categories":["Configuration Functions","Flash Functions","Miscellaneous Functions","Pagination Functions","Provides Functions","Rendering Functions"],"name":"Controller"},{"categories":["Date Functions","Miscellaneous Functions","String Functions"],"name":"Global Helpers"},{"categories":["General Functions","Migration Functions","Table Definition Functions"],"name":"Migrator"},{"categories":["Create Functions","CRUD Functions","Delete Functions","Miscellaneous Functions","Read Functions","Statistics Functions","Update Functions"],"name":"Model Class"},{"categories":["Association Functions","Callback Functions","Miscellaneous Functions","Validation Functions"],"name":"Model Configuration"},{"categories":["Change Functions","CRUD Functions","Error Functions","Miscellaneous Functions"],"name":"Model Object"},{"categories":["Callback Functions"],"name":"Test Model Configuration"},{"categories":["Testing Functions"],"name":"Test Model"},{"categories":["Asset Functions","Error Functions","Form Association Functions","Form Object Functions","Form Tag Functions","General Form Functions","Link Functions","Miscellaneous Functions","Sanitization Functions"],"name":"View Helpers"}],"functions":[{"extended":{"hasExtended":true,"docs":"

1. Allow only one property\n// In app/models/User.cfc\nfunction config() {\n    // Only allow `isActive` to be set through mass assignment\n    accessibleProperties("isActive");\n}\n\n// Example usage\nUser.updateAll(isActive=true);\n\n2. Allow multiple properties\n// In app/models/User.cfc\nfunction config() {\n    // Allow name and email to be set\n    accessibleProperties("firstName,lastName,email");\n}\n\n// Example usage\nUser.create(firstName="new", lastName="user", email="new@example.com");\n\n3. Dynamic restriction per model\n// In app/models/Post.cfc\nfunction config() {\n    if (application.env.environment == "production") {\n        // Lock down sensitive fields in production\n        accessibleProperties("title,content");\n    } else {\n        // In dev, keep it open for testing\n    }\n}
"},"hint":"Use this method inside your model’s config() function to whitelist which properties can be set via mass assignment operations (such as updateAll(), updateOne() and etc). This helps protect your model from accidental or malicious updates to sensitive fields (e.g., isAdmin, passwordHash, etc.).\n\n","returntype":"void","slug":"model.accessibleProperties","parameters":[{"default":"","required":false,"hint":"Property name (or list of property names) that are allowed to be altered through mass assignment.","name":"properties","type":"string"}],"availableIn":["model"],"name":"accessibleProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple string column\naddColumn(\n    table="members",\n    columnType="string",\n    columnName="status",\n    limit=50\n);\n\nAdds a status column (string, max 50 chars) to the members table.\n\n2. Add an integer column with default value\naddColumn(\n    table="orders",\n    columnType="integer",\n    columnName="priority",\n    default=0\n);\n\nAdds a priority column with default value 0.\n\n3. Add a boolean column that does not allow NULL\naddColumn(\n    table="users",\n    columnType="boolean",\n    columnName="isActive",\n    allowNull=false,\n    default=1\n);\n\nAdds an isActive column with default value true (1), disallowing NULL.\n\n4. Add a decimal column with precision and scale\naddColumn(\n    table="products",\n    columnType="decimal",\n    columnName="price",\n    precision=10,\n    scale=2\n);\n\nAdds a price column with up to 10 digits total, including 2 decimal places.\n\n5. Add a reference (foreign key) column\naddColumn(\n    table="orders",\n    columnType="reference",\n    columnName="userId",\n    referenceName="users"\n);\n\nAdds a userId column to orders and links it to the users table.
"},"hint":"Adds a new column to an existing table.\n This function is only available inside a migration CFC and is part of the Wheels migrator API. Use it to evolve your database schema safely through versioned migrations.\n\n","returntype":"void","slug":"migration.addColumn","parameters":[{"required":true,"hint":"The Name of the table to add the column to","name":"table","type":"string"},{"required":true,"hint":"The type of the new column","name":"columnType","type":"string"},{"default":"","required":true,"hint":"THe name of the new column","name":"columnName","type":"string"},{"default":"","required":false,"hint":"The name of the column which this column should be inserted after","name":"afterColumn","type":"string"},{"default":"","required":false,"hint":"Name for new reference column, see documentation for references function, required if columnType is 'reference'","name":"referenceName","type":"string"},{"required":false,"hint":"Default value for this column","name":"default","type":"string"},{"required":false,"hint":"Whether to allow NULL values","name":"allowNull","type":"boolean"},{"required":false,"hint":"Character or integer size limit for column","name":"limit","type":"numeric"},{"required":false,"hint":"precision value for decimal columns, i.e. number of digits the column can hold","name":"precision","type":"numeric"},{"required":false,"hint":"scale value for decimal columns, i.e. number of digits that can be placed to the right of the decimal point (must be less than or equal to precision)","name":"scale","type":"numeric"}],"availableIn":["migration"],"name":"addColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple error\n// In app/models/User.cfc\nthis.addError(\n    property="email",\n    message="Sorry, you are not allowed to use that email. Try again, please."\n);\n\nAdds an error on the email property.\n\n2. Add an error with a name identifier\nthis.addError(\n    property="password",\n    message="Password must contain at least one special character.",\n    name="weakPassword"\n);\n\nAdds a weakPassword error on the password property.\nLater you can check for it:\n\nif (user.hasError("password", "weakPassword")) {\n    // Handle specifically the weak password case\n}\n\n3. Adding multiple errors to the same property\nthis.addError(property="username", message="Username already taken.", name="duplicate");\nthis.addError(property="username", message="Username cannot contain spaces.", name="invalidChars");\n\nTwo different errors on username, each distinguished by their name.\n\n4. Conditional custom errors\n// Suppose only company emails are allowed\nif (!listLast(this.email, "@") == "company.com") {\n    this.addError(\n        property="email",\n        message="Please use your company email address.",\n        name="invalidDomain"\n    );\n}\n\nCustom rule ensures only company domain emails are accepted.\n\n5. Combine with built-in validations\n// Inside a callback\nfunction beforeSave() {\n    if (this.age < 18) {\n        this.addError(property="age", message="You must be at least 18 years old.");\n    }\n}\n\nEven though validatesPresenceOf("age") might exist, addError() gives you extra conditional control.
"},"hint":"Adds a custom error to a model instance. This is useful when built-in validations don’t fully cover your business rules, or when you want to enforce conditional logic. The error will be attached to the given property and can later be retrieved using functions like errorsOn() or allErrors().\n\n","returntype":"void","slug":"model.addError","parameters":[{"required":true,"hint":"The name of the property you want to add an error on.","name":"property","type":"string"},{"required":true,"hint":"The error message (such as \"Please enter a correct name in the form field\" for example).","name":"message","type":"string"},{"default":"","required":false,"hint":"A name to identify the error by (useful when you need to distinguish one error from another one set on the same object and you don't want to use the error message itself for that).","name":"name","type":"string"}],"availableIn":["model"],"name":"addError","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Add a general error\nthis.addErrorToBase(\n    message="Your email address must be the same as your domain name."\n);\n\nError applies to the whole object, not just email.\n\n2. Add a named error\nthis.addErrorToBase(\n    message="Order total must be greater than zero.",\n    name="invalidTotal"\n);\n\nUseful for distinguishing this error later when multiple base errors exist.\n\n3. Enforce a cross-property rule\nif (this.startDate > this.endDate) {\n    this.addErrorToBase(\n        message="Start date cannot be after end date.",\n        name="invalidDateRange"\n    );\n}\n\nRule depends on two properties, so the error belongs on the object as a whole.\n\n4. Business logic validation\nif (this.balance < this.minimumDeposit) {\n    this.addErrorToBase(\n        message="Balance is below the required minimum deposit.",\n        name="lowBalance"\n    );\n}\n\nExample where validation involves external business rules, not just a single column.\n\n5. Using with valid()\nif (!user.valid()) {\n    writeDump(user.allErrors());\n    // Will include base-level errors from addErrorToBase()\n}
"},"hint":"Adds an error directly on the model object itself, not tied to a specific property. This is useful when the error applies to the object as a whole or to a combination of properties, rather than a single field (for example: comparing two values, enforcing cross-property business rules, or validating external conditions).\n\n","returntype":"void","slug":"model.addErrorToBase","parameters":[{"required":true,"hint":"The error message (such as \"Please enter a correct name in the form field\" for example).","name":"message","type":"string"},{"default":"","required":false,"hint":"A name to identify the error by (useful when you need to distinguish one error from another one set on the same object and you don't want to use the error message itself for that).","name":"name","type":"string"}],"availableIn":["model"],"name":"addErrorToBase","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Basic foreign key\naddForeignKey(\n    table="orders",\n    referenceTable="users",\n    column="userId",\n    referenceColumn="id"\n);\n\nEnsures that every orders.userId must exist in users.id.\n\n2. Foreign key for many-to-one relation\naddForeignKey(\n    table="comments",\n    referenceTable="posts",\n    column="postId",\n    referenceColumn="id"\n);\n\nEnsures each comment is linked to a valid post.\n\n3. Foreign key with a custom reference column\naddForeignKey(\n    table="invoices",\n    referenceTable="customers",\n    column="customerCode",\n    referenceColumn="code"\n);\n\nLinks invoices.customerCode to customers.code instead of a numeric ID.\n\n4. Multiple foreign keys in one migration\n// In migration\naddForeignKey(\n    table="enrollments",\n    referenceTable="students",\n    column="studentId",\n    referenceColumn="id"\n);\n\naddForeignKey(\n    table="enrollments",\n    referenceTable="courses",\n    column="courseId",\n    referenceColumn="id"\n);\n\nThe enrollments table is linked to both students and courses.
"},"hint":"Adds a foreign key constraint between two tables. This ensures that values in one table’s column must exist in the referenced column of another table, enforcing referential integrity. This function is only available inside a migration CFC and is part of the Wheels migrator API.\n\n","returntype":"void","slug":"migration.addForeignKey","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"The reference table name to perform the operation on","name":"referenceTable","type":"string"},{"required":true,"hint":"The column name to perform the operation on","name":"column","type":"string"},{"required":true,"hint":"The reference column name to perform the operation on","name":"referenceColumn","type":"string"}],"availableIn":["migration"],"name":"addForeignKey","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a JavaScript format\naddFormat(\n    extension="js",\n    mimeType="text/javascript"\n);\n\nAllows controllers to respond to .js requests with the correct MIME type.\n\n2. Add PowerPoint formats\naddFormat(extension="ppt", mimeType="application/vnd.ms-powerpoint");\naddFormat(extension="pptx", mimeType="application/vnd.ms-powerpoint");\n\nEnables Wheels to correctly serve legacy and modern PowerPoint files.\n\n3. Add JSON format\naddFormat(\n    extension="json",\n    mimeType="application/json"\n);\n\nUseful for APIs that need to respond with .json requests.\n\n4. Add PDF format\naddFormat(\n    extension="pdf",\n    mimeType="application/pdf"\n);\n\nEnsures .pdf responses are correctly labeled for browsers.\n\n5. Add multiple custom data formats\naddFormat(extension="csv", mimeType="text/csv");\naddFormat(extension="yaml", mimeType="application/x-yaml");\n\nExpands your app to handle CSV and YAML outputs.
"},"hint":"Registers a new MIME type in your Wheels application for use with responding to multiple formats. This is helpful when your app needs to handle file types beyond the defaults provided by Wheels (e.g., serving JavaScript, PowerPoint, JSON, custom data formats).\n\n","returntype":"void","slug":"controller.addFormat","parameters":[{"required":true,"hint":"File extension to add.","name":"extension","type":"string"},{"required":true,"hint":"Matching MIME type to associate with the file extension.","name":"mimeType","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"addFormat","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"configuration","category":"Miscellaneous Functions","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a unique index on a single column\naddIndex(\n    table="members",\n    columnNames="username",\n    unique=true\n);\n\nEnsures username values in members are unique.\n\n2. Add a non-unique index for faster queries\naddIndex(\n    table="orders",\n    columnNames="createdAt"\n);\n\nSpeeds up queries filtering or ordering by createdAt.\n\n3. Add a composite index (multiple columns)\naddIndex(\n    table="posts",\n    columnNames="authorId,createdAt"\n);\n\nOptimizes queries that filter or sort on both authorId and createdAt.\n\n4. Add an index with a custom name\naddIndex(\n    table="comments",\n    columnNames="postId",\n    indexName="idx_comments_postId"\n);\n\nCreates index with a custom name instead of default comments_postId.\n\n5. Composite unique index\naddIndex(\n    table="enrollments",\n    columnNames="studentId,courseId",\n    unique=true,\n    indexName="unique_enrollments"\n);\n\nPrevents the same studentId and courseId pair from being inserted more than once.
"},"hint":"Adds a database index on one or more columns of a table. Indexes speed up queries that filter, sort, or join on those columns. This function is only available inside a migration CFC and is part of the Wheels migrator API.\n\n","returntype":"void","slug":"migration.addIndex","parameters":[{"required":true,"hint":"The table name to perform the index operation on","name":"table","type":"string"},{"required":false,"hint":"One or more column names to index, comma separated","name":"columnNames","type":"string"},{"default":"false","required":false,"hint":"If true will create a unique index constraint","name":"unique","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"The name of the index to add: Defaults to table name + underscore + first column name","name":"indexName","type":"string"}],"availableIn":["migration"],"name":"addIndex","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single record\naddRecord(\n    table="people",\n    id=1,\n    title="Mr",\n    firstname="Bruce",\n    lastname="Wayne", \n    email="bruce@wayneenterprises.com",\n    tel="555-67869099"\n);\n\nInserts one record into the people table.\n\n2. Add a record with only required fields\naddRecord(\n    table="roles",\n    id=1,\n    name="Admin"\n);\n\nSeeds an Admin role into the roles table.\n\n3. Add a record with default values in schema\naddRecord(\n    table="users",\n    email="new@example.com",\n    firstName="new",\n    lastName="user"\n);\n\nRelies on schema defaults (e.g., isActive=true) for missing fields.\n\n4. Add lookup data\naddRecord(\n    table="statuses",\n    id=1,\n    name="Pending"\n);\naddRecord(\n    table="statuses",\n    id=2,\n    name="Approved"\n);\naddRecord(\n    table="statuses",\n    id=3,\n    name="Rejected"\n);\n\nSeeds reusable lookup/status values.\n\n5. Add a record referencing another table\n// Assuming user with ID=1 exists\naddRecord(\n    table="posts",\n    id=1,\n    title="First Post",\n    content="Hello, Wheels!",\n    userId=1\n);\n\nCreates a post tied to an existing user.
"},"hint":"Inserts a new record into a table. This function is only available inside a migration CFC and is part of the Wheels migrator API. Useful for seeding initial data (like admin users, roles, or lookup values) alongside schema changes.\n\n","returntype":"void","slug":"migration.addRecord","parameters":[{"required":true,"hint":"The table name to add the record to","name":"table","type":"string"}],"availableIn":["migration"],"name":"addRecord","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a user reference to orders\naddReference(\n    table="orders",\n    referenceName="users"\n);\n\nAdds a userId column to orders and creates a foreign key to users.id.\n\n2. Add a post reference to comments\naddReference(\n    table="comments",\n    referenceName="posts"\n);\n\nCreates a postId column on comments and links it to posts.id.\n\n3. Add references to multiple tables\naddReference(table="enrollments", referenceName="students");\naddReference(table="enrollments", referenceName="courses");\n\nAdds both studentId and courseId to enrollments with foreign keys to students and courses.\n\n4. Composite example (reference + other fields)\naddColumn(table="votes", columnType="boolean", columnName="upvote", default=1);\naddReference(table="votes", referenceName="users");\naddReference(table="votes", referenceName="posts");\n\nBuilds a votes table that connects users and posts with foreign keys.
"},"hint":"Adds a reference column and a foreign key constraint to a table in one step. This is a shortcut for creating an integer column (e.g., userId) and then linking it to another table using a foreign key. This function is only available inside a migration CFC and is part of the Wheels migrator API.\n\n","returntype":"void","slug":"migration.addReference","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"The reference table name to perform the operation on","name":"referenceName","type":"string"}],"availableIn":["migration"],"name":"addReference","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Single callback method\n// Instruct Wheels to call the `fixObj` method after an object is created\nafterCreate(\"fixObj\");\n\nfunction fixObj() {\n    variables.fixed = true;\n}\n\n2. Multiple callbacks\nafterCreate(\"logCreation,notifyAdmin\");\n\nfunction logCreation() {\n    writeLog(\"New record created at #now()#\");\n}\n\nfunction notifyAdmin() {\n    // send an email notification\n}\n\n3. With object attributes\nafterCreate(\"setDefaults\");\n\nfunction setDefaults() {\n    if (!len(variables.status)) {\n        variables.status = \"pending\";\n    }\n}\n\n4. Practical usage in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterCreate(\"assignRole,sendWelcomeEmail\");\n    }\n\n    function assignRole() {\n        if (isNull(roleId)) {\n            roleId = Role.findOneByName(\"User\").id;\n        }\n    }\n\n    function sendWelcomeEmail() {\n        // code to send welcome email\n    }\n}
"},"hint":"Registers one or more callback methods that are automatically executed after a new object is created (i.e., after calling create() on a model). This is part of the model lifecycle callbacks in Wheels.\n\n","returntype":"void","slug":"model.afterCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Single callback method\n// Call `logDeletion` after an object is deleted\nafterDelete(\"logDeletion\");\n\nfunction logDeletion() {\n    writeLog(\"Record deleted at #now()#\");\n}\n\n2. Multiple callbacks\nafterDelete(\"archiveData,notifyAdmin\");\n\nfunction archiveData() {\n    // move deleted data to an archive table\n}\n\nfunction notifyAdmin() {\n    // send a notification email\n}\n\n3. With related cleanup\nafterDelete(\"removeAssociatedRecords\");\n\nfunction removeAssociatedRecords() {\n    // remove orphaned child records manually\n    Order.deleteAll(where=\"userId = #this.id#\");\n}\n\n4. Practical usage in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterDelete(\"cleanupSessions,sendGoodbyeEmail\");\n    }\n\n    function cleanupSessions() {\n        Session.deleteAll(where=\"userId = #id#\");\n    }\n\n    function sendGoodbyeEmail() {\n        // code to send a farewell email\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object is deleted from the database. This hook allows you to perform cleanup, logging, or side effects when a record has been removed.\n\n","returntype":"void","slug":"model.afterDelete","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterDelete","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a timestamp when data was fetched\ncomponent extends=\"Model\" {\n    function config() {\n        afterFind(\"setTime\");\n    }\n\n    function setTime() {\n        arguments.fetchedAt = now();\n        return arguments;\n    }\n}\n\nWhen you call:\n\nuser = model(\"User\").findByKey(1);\nwriteOutput(user.fetchedAt); // Shows the time record was retrieved\n\n2. Format or normalize data\nafterFind(\"normalizeEmail\");\n\nfunction normalizeEmail() {\n    this.email = lcase(this.email);\n}\n\nEnsures all email addresses are lowercased when loaded.\n\n3. Load related info automatically\nafterFind(\"attachProfile\");\n\nfunction attachProfile() {\n    this.profile = model(\"Profile\").findOne(where=\"userId = #this.id#\");\n}\n\nNow every User object automatically has its related profile loaded.\n\n4. Multiple callbacks\nafterFind(\"setTime,normalizeEmail,attachProfile\");\n\nAll three methods will run in order after the object is retrieved.
"},"hint":"Registers one or more callback methods that should be executed after an existing object has been initialized, typically via finder methods such as findByKey, findOne, findAll, or other query-based lookups. This hook is useful for adjusting, enriching, or transforming model objects immediately after they are loaded from the database.\n\n","returntype":"void","slug":"model.afterFind","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterFind","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Normalize data after every initialization\nafterInitialization(\"normalizeName\");\n\nfunction normalizeName() {\n    this.firstName = trim(this.firstName);\n    this.lastName = trim(this.lastName);\n}\n\nEnsures whitespace is stripped whether the object is new or fetched.\n\n2. Add a helper attribute for all instances\nafterInitialization(\"addFullName\");\n\nfunction addFullName() {\n    this.fullName = this.firstName & \" \" & this.lastName;\n}\n\nNow every object has a fullName property set right after creation or retrieval.\n\n3. Multiple callbacks\nafterInitialization(\"normalizeName,addFullName\");\n\nRuns both methods sequentially.\n\n4. Practical example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterInitialization(\"normalizeName,addFullName,setFetchedAt\");\n    }\n\n    function normalizeName() {\n        this.firstName = trim(this.firstName);\n        this.lastName = trim(this.lastName);\n    }\n\n    function addFullName() {\n        this.fullName = this.firstName & \" \" & this.lastName;\n    }\n\n    function setFetchedAt() {\n        arguments.fetchedAt = now();\n        return arguments;\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object has been initialized. Initialization happens in two cases, When a new object is created (via new() or similar) or when an existing object is fetched from the database (via findByKey, findOne, etc.). This makes afterInitialization() more general than afterCreate() or afterFind(), since it runs in both scenarios.\n\n","returntype":"void","slug":"model.afterInitialization","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterInitialization","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Set default values for new records\nafterNew(\"setDefaults\");\n\nfunction setDefaults() {\n    this.isActive = true;\n    this.role = \"member\";\n}\n\nWhenever a new object is initialized, default values are assigned.\n\n2. Generate a temporary property\nafterNew(\"assignTempId\");\n\nfunction assignTempId() {\n    this.tempId = createUUID();\n}\n\nEach new object will have a unique tempId until it’s saved.\n\n3. Multiple callbacks\nafterNew(\"setDefaults,assignTempId\");\n\nRuns both methods sequentially for every new object.\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterNew(\"setDefaults,prepareDisplayName\");\n    }\n\n    function setDefaults() {\n        this.isActive = true;\n    }\n\n    function prepareDisplayName() {\n        this.displayName = this.firstName & \" \" & this.lastName;\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after a new object has been initialized, typically via the new() method. This hook is useful for setting default values, preparing derived attributes, or running logic every time you create a fresh model instance (before saving it to the database).\n\n","returntype":"void","slug":"model.afterNew","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterNew","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Log every save\nafterSave(\"logSave\");\n\nfunction logSave() {\n    writeLog(\"User ##this.id## saved at #now()#\");\n}\n\n2. Trigger notifications\nafterSave(\"notifyAdmin\");\n\nfunction notifyAdmin() {\n    if (this.role == \"admin\") {\n        sendEmail(to=\"superadmin@example.com\", subject=\"Admin Updated\", body=\"Admin user #this.id# has been updated.\");\n    }\n}\n\n3. Multiple callbacks\nafterSave(\"logSave,notifyAdmin\");\n\n4. Example in Order.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterSave(\"recalculateInventory,sendConfirmation\");\n    }\n\n    function recalculateInventory() {\n        Inventory.updateStock(this.productId, -this.quantity);\n    }\n\n    function sendConfirmation() {\n        EmailService.sendOrderConfirmation(this.id);\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object is saved to the database. This hook runs whether the save was the result of creating a new record or updating an existing one. It’s ideal for tasks that must happen after persistence, such as logging, syncing data, or triggering external processes.\n\n","returntype":"void","slug":"model.afterSave","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterSave","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Simple logging\nafterUpdate(\"logUpdate\");\n\nfunction logUpdate() {\n    writeLog(\"Record ##this.id## was updated at #now()#\");\n}\n\n2. Trigger an email when a specific field changes\nafterUpdate(\"notifyEmailChange\");\n\nfunction notifyEmailChange() {\n    if (this.hasChanged(\"email\")) {\n        sendEmail(\n            to=this.email,\n            subject=\"Your email was updated\",\n            body=\"Hi #this.firstName#, your email address has been changed.\"\n        );\n    }\n}\n\n3. Multiple callbacks\nafterUpdate(\"logUpdate,notifyEmailChange\");\n\n4. Example in Order.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterUpdate(\"updateInventory,sendUpdateNotification\");\n    }\n\n    function updateInventory() {\n        Inventory.adjustStock(this.productId, -this.quantity);\n    }\n\n    function sendUpdateNotification() {\n        EmailService.sendOrderUpdate(this.id);\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an existing object has been updated in the database. This hook is ideal for performing follow-up tasks whenever a record changes — such as logging, cache invalidation, or sending notifications about updates.\n\n","returntype":"void","slug":"model.afterUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a custom validation error\nafterValidation(\"checkRestrictedEmails\");\n\nfunction checkRestrictedEmails() {\n    if (listFindNoCase(\"test@example.com,admin@example.com\", this.email)) {\n        this.addError(\"email\", \"That email address is not allowed.\");\n    }\n}\n\n2. Normalize data after validation\nafterValidation(\"normalizePhone\");\n\nfunction normalizePhone() {\n    if (len(this.phone)) {\n        this.phone = rereplace(this.phone, \"[^0-9]\", \"\", \"all\");\n    }\n}\n\n3. Multiple callbacks\nafterValidation(\"checkRestrictedEmails,normalizePhone\");\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        validatesPresenceOf(\"email\");\n        afterValidation(\"checkRestrictedEmails,normalizePhone\");\n    }\n\n    function checkRestrictedEmails() {\n        if (listFindNoCase(\"banned@example.com\", this.email)) {\n            this.addError(\"email\", \"This email address is not permitted.\");\n        }\n    }\n\n    function normalizePhone() {\n        this.phone = rereplace(this.phone, \"[^0-9]\", \"\", \"all\");\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object has been validated. This hook is useful for running extra logic that depends on validation results, such as adjusting error messages, performing side validations, or preparing data before saving.\n\n","returntype":"void","slug":"model.afterValidation","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterValidation","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a creation-only error\nafterValidationOnCreate(\"checkSignupEmail\");\n\nfunction checkSignupEmail() {\n    if (listFindNoCase(\"banned@example.com,blocked@example.com\", this.email)) {\n        this.addError(\"email\", \"This email address cannot be used for registration.\");\n    }\n}\n\n2. Generate a default username if missing\nafterValidationOnCreate(\"generateUsername\");\n\nfunction generateUsername() {\n    if (!len(this.username)) {\n        this.username = listFirst(this.email, \"@\");\n    }\n}\n\n3. Multiple callbacks\nafterValidationOnCreate(\"checkSignupEmail,generateUsername\");\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        validatesPresenceOf(\"email\");\n        validatesFormatOf(property=\"email\", regex=\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\");\n\n        afterValidationOnCreate(\"checkSignupEmail,generateUsername\");\n    }\n\n    function checkSignupEmail() {\n        if (listFindNoCase(\"banned@example.com\", this.email)) {\n            this.addError(\"email\", \"This email address is restricted.\");\n        }\n    }\n\n    function generateUsername() {\n        if (!len(this.username)) {\n            this.username = listFirst(this.email, \"@\");\n        }\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after a new object has been validated (i.e., when running validations during a create() or save() on a new record). This hook is useful when you want to apply custom logic only during new record creation, not during updates.\n\n","returntype":"void","slug":"model.afterValidationOnCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterValidationOnCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Prevent updating restricted emails\nafterValidationOnUpdate(\"checkRestrictedEmail\");\n\nfunction checkRestrictedEmail() {\n    if (this.email eq \"admin@example.com\") {\n        this.addError(\"email\", \"You cannot change this email address.\");\n    }\n}\n\n2. Automatically update a lastModifiedBy field\nafterValidationOnUpdate(\"setLastModifiedBy\");\n\nfunction setLastModifiedBy() {\n    this.lastModifiedBy = session.userId;\n}\n\n3. Multiple callbacks\nafterValidationOnUpdate(\"checkRestrictedEmail,setLastModifiedBy\");\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        validatesPresenceOf(\"email\");\n        validatesFormatOf(property=\"email\", regex=\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\");\n\n        afterValidationOnUpdate(\"checkRestrictedEmail,setLastModifiedBy\");\n    }\n\n    function checkRestrictedEmail() {\n        if (this.email eq \"admin@example.com\") {\n            this.addError(\"email\", \"This email cannot be changed.\");\n        }\n    }\n\n    function setLastModifiedBy() {\n        this.lastModifiedBy = session.userId;\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an existing object has been validated (i.e., when running validations during an update() or save() on an already-persisted record). This hook is useful when you want logic to run only on updates, not on initial creation.\n\n","returntype":"void","slug":"model.afterValidationOnUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterValidationOnUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\nmember = model(\"member\").findByKey(params.memberId);\n\n// Change some values (not saved yet)\nmember.firstName = params.newFirstName;\nmember.email = params.newEmail;\n\n// Get all pending changes\nallChanges = member.allChanges();\n// Example output: {\"email\":{\"CHANGEDTO\":\"old@gmail.com\",\"CHANGEDFROM\":\"new@gmail.com\"},\"firstname\":{\"CHANGEDTO\":\"old\",\"CHANGEDFROM\":\"new\"}}\n\n2. Checking if changes exist before saving\nmember = model(\"member\").findByKey(42);\nmember.status = \"inactive\";\n\nif (!structIsEmpty(member.allChanges())) {\n    writeDump(var=member.allChanges(), label=\"Pending Changes\");\n    member.save();\n}\n\n3. Using in a validation callback\nafterValidation(\"logChanges\");\n\nfunction logChanges() {\n    var changes = this.allChanges();\n    if (!structIsEmpty(changes)) {\n        log(message=\"User ##this.id## updated fields: #structKeyList(changes)#\");\n    }\n}\n\n4. Example with multiple updates\nuser = model(\"user\").findByKey(10);\n\nuser.firstName = \"Jane\";\nuser.lastName  = \"Doe\";\nuser.email     = \"jane.doe@example.com\";\n\nchanges = user.allChanges();\n// Output might be: {\"email\":{\"CHANGEDTO\":\"jane.doe@example.com\",\"CHANGEDFROM\":\"example.user@gmail.com\"},\"lastname\":{\"CHANGEDTO\":\"Doe\",\"CHANGEDFROM\":\"user\"},\"firstname\":{\"CHANGEDTO\":\"Jane\",\"CHANGEDFROM\":\"example\"}}
"},"hint":"Returns a struct containing all unsaved changes made to an object since it was last loaded or saved. Each entry in the struct uses the property name as the key and the new (unsaved) value as the value.\n\n","returntype":"struct","slug":"model.allChanges","parameters":[],"availableIn":["model"],"name":"allChanges","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Get all validation errors\nuser = model(\"user\").new(\n    username = \"\",\n    password = \"\"\n);\n\n// Validate the object\nuser.valid();\n\n// Fetch errors\nerrorInfo = user.allErrors();\n\nwriteDump(var=errorInfo, label=\"User Errors\");\n\nSample output:\n\n[\n  {\n    \"message\": \"Username must not be blank.\",\n    \"name\": \"PresenceOf\",\n    \"property\": \"username\"\n  },\n  {\n    \"message\": \"Password must not be blank.\",\n    \"name\": \"PresenceOf\",\n    \"property\": \"password\"\n  }\n]\n\n2. Including associated model errors\norder = model(\"order\").new(\n    customer = model(\"customer\").new(name=\"\")\n);\n\n// Validate both order and associated customer\norder.valid();\n\n// Get errors from both order and customer\nerrors = order.allErrors(includeAssociations=true);\n\n3. Checking for errors before saving\nuser = model(\"user\").new(email=\"not-an-email\");\n\nif (!user.valid()) {\n    errors = user.allErrors();\n    for (err in errors) {\n        writeOutput(\"Error on #err.property#: #err.message#\");\n    }\n}
"},"hint":"Returns an array of all the errors on the object.\n\n\nIt does this by storing instances of models that are associations, and not checking associations of those instances because they have already been checked.","returntype":"array","slug":"model.allErrors","parameters":[{"default":false,"required":false,"name":"includeAssociations","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"is a private argument not meant to be used by the user, the function uses this to ensure circular dependency avoidance.","name":"seenErrors","type":"array"}],"availableIn":["model"],"name":"allErrors","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Announce a step in a migration\nannounce(\"Adding status column to members table...\");\naddColumn(\n    table = \"members\",\n    columnType = \"string\",\n    columnName = \"status\",\n    limit = 50\n);\n\n2. Announce progress in multiple steps\nannounce(\"Creating orders table...\");\ncreateTable(\"orders\", function(table) {\n    table.integer(\"id\");\n    table.string(\"description\");\n});\n\nannounce(\"Adding index on orders.description...\");\naddIndex(table=\"orders\", columnNames=\"description\");\n\n3. Use for debugging migrations\nannounce(\"Starting migration at #Now()#\");\n\n// Migration logic here...\n\nannounce(\"Migration completed successfully.\");
"},"hint":"Outputs a custom message during migration execution. This is useful for logging progress or providing context when multiple migration steps are running.\n\n","returntype":"any","slug":"migration.announce","parameters":[{"required":true,"name":"message","type":"string"}],"availableIn":["migration","tabledefinition"],"name":"announce","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Basic true assertion\n// Passes because 2 + 2 = 4\nassert(\"2 + 2 EQ 4\");\n\n2. Assertion that fails\n// This will fail the test because 5 is not less than 3\nassert(\"5 LT 3\");\n\n3. With model object conditions\nuser = model(\"user\").findByKey(1);\n\n// Assert that the user has an email set\nassert(len(user.email));
"},"hint":"Asserts that an expression evaluates to true in a test. If the expression evaluates to false, the test will fail and an error will be raised. This is one of the core testing functions available when writing legacy tests in Wheels.","returntype":"void","slug":"test.assert","parameters":[{"required":true,"name":"expression","type":"string"}],"availableIn":["test"],"name":"assert","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Get the raw CSRF token in a controller\ntoken = authenticityToken();\n\n2. Output token manually in a form (not recommended, but possible)\n<form action=\"/posts/create\" method=\"post\">\n    <input type=\"hidden\" name=\"authenticityToken\" value=\"#authenticityToken()#\">\n    <input type=\"text\" name=\"title\">\n    <input type=\"submit\" value=\"Save\">\n</form>\n\n3. Use in AJAX request headers\nfetch(\"/posts/create\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    \"X-CSRF-Token\": \"#authenticityToken()#\"\n  },\n  body: JSON.stringify({ title: \"New Post\" })\n});
"},"hint":"Returns the raw CSRF authenticity token for the current user session. This token is used to help protect against Cross-Site Request Forgery (CSRF) attacks by verifying that form submissions or AJAX requests originate from your application. You typically won’t call this function directly in views — instead, Wheels provides helpers like authenticityTokenField() to generate hidden form fields. But authenticityToken() can be useful if you need direct access to the token string (for example, in custom JavaScript code).\n\n","returntype":"string","slug":"controller.authenticityToken","parameters":[],"availableIn":["controller"],"name":"authenticityToken","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Adding a CSRF token to a manual form\n<!--- Needed here because we're not using startFormTag --->\n<form action=\"#urlFor(route='posts')#\" method=\"post\">\n  #authenticityTokenField()#\n  <input type=\"text\" name=\"title\">\n  <input type=\"submit\" value=\"Create Post\">\n</form>\n\n2. No token needed for safe GET forms\n<!--- Not needed here because GET requests are not protected --->\n<form action=\"#urlFor(route='invoices')#\" method=\"get\">\n  <input type=\"text\" name=\"search\">\n  <input type=\"submit\" value=\"Find Invoice\">\n</form>\n\n3. Custom AJAX form with CSRF token\n<form id=\"ajaxForm\">\n  #authenticityTokenField()#\n  <input type=\"text\" name=\"title\">\n  <button type=\"submit\">Save</button>\n</form>\n\ndocument.getElementById(\"ajaxForm\").addEventListener(\"submit\", function(e) {\n  e.preventDefault();\n\n  const token = document.querySelector(\"input[name='authenticityToken']\").value;\n\n  fetch(\"/posts\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"X-CSRF-Token\": token\n    },\n    body: JSON.stringify({ title: \"CSRF-protected post\" })\n  });\n});
"},"hint":"Generates a hidden form field that contains a CSRF authenticity token. This token is required for verifying that POST, PUT, PATCH, or DELETE requests originated from your application, helping protect against Cross-Site Request Forgery (CSRF) attacks. When you use startFormTag(), Wheels automatically includes the token field for you. You’ll usually only need to call authenticityTokenField() manually when creating forms without startFormTag() or when building raw HTML forms.\n\n","returntype":"string","slug":"controller.authenticityTokenField","parameters":[],"availableIn":["controller"],"name":"authenticityTokenField","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Auto-link a URL\n#autoLink(\"Download Wheels from https://wheels.dev\")#\n\nOutput:\n\nDownload Wheels from <a href=\"https://wheels.dev\">https://wheels.dev</a>\n\n2. Auto-link an email address\n#autoLink(\"Email us at info@cfwheels.org\")#\n\nOutput:\n\nEmail us at <a href=\"mailto:info@cfwheels.org\">info@cfwheels.org</a>\n\n3. Only link URLs, not emails\n#autoLink(text=\"Visit https://cfwheels.org or email support@cfwheels.org\", link=\"URLs\")#\n\nOutput:\n\nVisit <a href=\"https://cfwheels.org\">https://cfwheels.org</a> or email support@cfwheels.org\n\n4. Only link email addresses\n#autoLink(text=\"Contact info@cfwheels.org or see https://cfwheels.org\", link=\"emailAddresses\")#\n\nOutput:\n\nContact <a href=\"mailto:info@cfwheels.org\">info@cfwheels.org</a> or see https://cfwheels.org\n\n5. Disable auto-linking of relative URLs\n#autoLink(text=\"See /about for more info\", relative=false)#\n\nOutput:\n\nSee /about for more info
"},"hint":"Scans a block of text for URLs and/or email addresses and automatically converts them into clickable links. This helper is handy for displaying user-generated content, comments, or messages where you want to make links interactive without manually adding
tags.\n\n","returntype":"string","slug":"controller.autoLink","parameters":[{"required":true,"hint":"The text to create links in.","name":"text","type":"string"},{"default":"all","required":false,"hint":"Whether to link URLs, email addresses or both. Possible values are: `all` (default), `URLs` and `emailAddresses`.","name":"link","type":"string"},{"default":true,"required":false,"hint":"Should we auto-link relative urls.","name":"relative","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"autoLink","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Disable automatic validations for a single model\ncomponent extends=\"Model\" {\n    function config() {\n        automaticValidations(false);\n    }\n}\n\n\nUseful when automatic validations are enabled globally but a model requires custom validation handling.\n\n2. Enable automatic validations explicitly for a model\ncomponent extends=\"Model\" {\n    function config() {\n        automaticValidations(true);\n    }\n}\n\n\nEnsures this model always applies database-inferred validations, even if global automatic validations are turned off.\n\n3. Combining with custom validations\ncomponent extends=\"Model\" {\n    function config() {\n        automaticValidations(false); // turn off inferred rules\n        validatesPresenceOf(\"email\");\n        validatesFormatOf(property=\"email\", regex=\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\");\n    }\n}\n\n\nHere, automatic validations are disabled, but explicit validation rules are still applied.
"},"hint":"Controls whether automatic validations should be enabled for a specific model. By default, Wheels can automatically infer validations from your database schema (e.g., NOT NULL fields, field length limits, etc.). This function lets you override that behavior at the model level — enabling or disabling automatic validations regardless of the global setting.\n\n","returntype":"void","slug":"model.automaticValidations","parameters":[{"required":true,"hint":"Set to `true` or `false`.","name":"value","type":"boolean"}],"availableIn":["model"],"name":"automaticValidations","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Average salary for all employees\navgSalary = model(\"employee\").average(\"salary\");\n\n2. Average salary filtered by department\navgSalary = model(\"employee\").average(\n    property = \"salary\",\n    where    = \"departmentId = #params.key#\"\n);\n\n3. Ensure a numeric value is always returned\navgSalary = model(\"employee\").average(\n    property = \"salary\",\n    where    = \"salary BETWEEN #params.min# AND #params.max#\",\n    ifNull   = 0\n);\n\n4. Average with distinct values only\navgSalary = model(\"employee\").average(\n    property = \"salary\",\n    distinct = true\n);\n\n5. Grouped average by department\navgSalaries = model(\"employee\").average(\n    property = \"salary\",\n    group    = \"departmentId\"\n);
"},"hint":"Calculates the average value for a given property.\nUses the SQL function AVG.\nIf no records can be found to perform the calculation on you can use the ifNull argument to decide what should be returned.\n\n","returntype":"any","slug":"model.average","parameters":[{"required":true,"hint":"Name of the property to calculate the average for.","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":false,"required":false,"hint":"When `true`, `AVG` will be performed only on each unique instance of a value, regardless of how many times the value occurs.","name":"distinct","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"average","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Run a method before saving a new object\nfunction config() {\n    beforeCreate(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Ensure a default role is assigned\n    if (!structKeyExists(this, \"roleId\")) {\n        this.roleId = 2; // Assign \"user\" role\n    }\n}\n\n2. Generate a unique slug before creation\nfunction config() {\n    beforeCreate(\"generateSlug\");\n}\n\nfunction generateSlug() {\n    this.slug = lcase(replace(this.title, \" \", \"-\", \"all\"));\n}\n\n3. Hash a password before inserting a new user\nfunction config() {\n    beforeCreate(\"hashPassword\");\n}\n\nfunction hashPassword() {\n    this.password = hash(this.password, \"SHA-256\");\n}
"},"hint":"Registers method(s) that should be called before a new object is created. This allows you to modify or validate data, set defaults, or perform logic right before the object is persisted in the database for the first time.\n\n","returntype":"void","slug":"model.beforeCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: run a method before deleting\nfunction config() {\n    beforeDelete(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: log deletions\n    writeLog(\"Deleting record with ID #this.id#\");\n}\n\n2. Prevent deletion if conditions fail\nfunction config() {\n    beforeDelete(\"checkIfAdmin\");\n}\n\nfunction checkIfAdmin() {\n    if (!session.isAdmin) {\n        throw(type=\"SecurityException\", message=\"Only admins can delete records.\");\n    }\n}\n\n3. Cascade cleanup before deletion\nfunction config() {\n    beforeDelete(\"cleanupAssociations\");\n}\n\nfunction cleanupAssociations() {\n    // Delete related comments before removing a post\n    model(\"comment\").deleteAll(where=\"postId = #this.id#\");\n}
"},"hint":"Registers method(s) that should be called before an object is deleted. This allows you to perform cleanup, enforce constraints, or prevent deletion if certain conditions are not met.\n\n","returntype":"void","slug":"model.beforeDelete","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeDelete","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: run a method before save\nfunction config() {\n    beforeSave(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: Trim whitespace before saving\n    this.username = trim(this.username);\n}\n\n2. Automatically update a timestamp\nfunction config() {\n    beforeSave(\"updateTimestamp\");\n}\n\nfunction updateTimestamp() {\n    this.lastModifiedAt = now();\n}\n\n3. Normalize data before saving\nfunction config() {\n    beforeSave(\"normalizeData\");\n}\n\nfunction normalizeData() {\n    // Example: ensure email is lowercase\n    this.email = lcase(this.email);\n\n    // Example: capitalize first name\n    this.firstName = ucase(left(this.firstName, 1)) & mid(this.firstName, 2);\n}\n\n4. Prevent save if conditions fail\nfunction config() {\n    beforeSave(\"blockInactiveUsers\");\n}\n\nfunction blockInactiveUsers() {\n    if (!this.isActive) {\n        throw(type=\"ValidationException\", message=\"Inactive users cannot be saved.\");\n    }\n}
"},"hint":"Registers method(s) that should be called before an object is saved. This is useful for performing transformations, validations, or logging before data is persisted.\n\n","returntype":"void","slug":"model.beforeSave","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeSave","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before update\nfunction config() {\n    beforeUpdate(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: trim whitespace before updating\n    this.lastName = trim(this.lastName);\n}\n\n2. Update an \\\"last modified\\\" timestamp\nfunction config() {\n    beforeUpdate(\"updateTimestamp\");\n}\n\nfunction updateTimestamp() {\n    this.updatedAt = now();\n}\n\n3. Prevent updating sensitive fields\nfunction config() {\n    beforeUpdate(\"restrictEmailChange\");\n}\n\nfunction restrictEmailChange() {\n    if (this.hasChanged(\"email\")) {\n        throw(type=\"ValidationException\", message=\"Email address cannot be changed.\");\n    }\n}\n\n4. Audit updates with logging\nfunction config() {\n    beforeUpdate(\"logChanges\");\n}\n\nfunction logChanges() {\n    var changes = this.allChanges();\n    writeLog(text=\"User ##this.id## updated with changes: #serializeJSON(changes)#\", file=\"audit\");\n}
"},"hint":"Registers method(s) that should be called before an existing object is updated. This is useful for enforcing rules, transforming values, or checking conditions specifically for update operations (unlike beforeSave(), which applies to both create and update).\n\n","returntype":"void","slug":"model.beforeUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before validation\nfunction config() {\n    beforeValidation(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: normalize names before validation\n    this.firstName = trim(this.firstName);\n    this.lastName = trim(this.lastName);\n}\n\n2. Ensure default values before validation\nfunction config() {\n    beforeValidation(\"setDefaults\");\n}\n\nfunction setDefaults() {\n    if (!len(this.status)) {\n        this.status = \"pending\";\n    }\n}\n\n3. Convert input formats before validating\nfunction config() {\n    beforeValidation(\"normalizePhone\");\n}\n\nfunction normalizePhone() {\n    // Remove spaces/dashes so the validation regex can run correctly\n    this.phoneNumber = rereplace(this.phoneNumber, \"[^0-9]\", \"\", \"all\");\n}\n\n4. Multi-method callback\nfunction config() {\n    beforeValidation(\"sanitizeEmail, normalizeUsername\");\n}\n\nfunction sanitizeEmail() {\n    this.email = lcase(trim(this.email));\n}\n\nfunction normalizeUsername() {\n    this.username = rereplace(this.username, \"[^a-zA-Z0-9]\", \"\", \"all\");\n}
"},"hint":"Registers method(s) that should be called before an object is validated. This hook is helpful when you want to adjust, normalize, or clean up data before validation rules run. It ensures the object is in the correct state so that validations pass or fail as expected.\n\n","returntype":"void","slug":"model.beforeValidation","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeValidation","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before validation on create\nfunction config() {\n    beforeValidationOnCreate(\"fixObj\");\n}\n\nfunction fixObj() {\n    this.firstName = trim(this.firstName);\n}\n\n2. Ensure default values only for new records\nfunction config() {\n    beforeValidationOnCreate(\"setDefaults\");\n}\n\nfunction setDefaults() {\n    if (!len(this.role)) {\n        this.role = \"member\";\n    }\n}\n\n3. Normalize data formats for new users\nfunction config() {\n    beforeValidationOnCreate(\"normalizeNewUserData\");\n}\n\nfunction normalizeNewUserData() {\n    // Make sure emails are stored lowercase for new accounts\n    this.email = lcase(trim(this.email));\n}\n\n4. Run multiple setup methods before new record validation\nfunction config() {\n    beforeValidationOnCreate(\"assignUUID, sanitizeName\");\n}\n\nfunction assignUUID() {\n    if (!len(this.uuid)) {\n        this.uuid = createUUID();\n    }\n}\n\nfunction sanitizeName() {\n    this.fullName = trim(this.fullName);\n}
"},"hint":"Registers method(s) that should be called before a new object is validated. This hook is useful when you want to prepare or sanitize data specifically for new records, ensuring that validations run on properly formatted data. It will not run on updates—only on create() or new() + save() operations.\n\n","returntype":"void","slug":"model.beforeValidationOnCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeValidationOnCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before validation on update\nfunction config() {\n    beforeValidationOnUpdate(\"fixObj\");\n}\n\nfunction fixObj() {\n    this.lastName = trim(this.lastName);\n}\n\n2. Prevent changes to immutable fields\nfunction config() {\n    beforeValidationOnUpdate(\"restrictImmutableFields\");\n}\n\nfunction restrictImmutableFields() {\n    if (this.hasChanged(\"email\")) {\n        this.addError(property=\"email\", message=\"Email cannot be changed once set.\");\n    }\n}\n\n3. Normalize input before update validations\nfunction config() {\n    beforeValidationOnUpdate(\"sanitizePhone\");\n}\n\nfunction sanitizePhone() {\n    this.phoneNumber = rereplace(this.phoneNumber, \"[^0-9]\", \"\", \"all\");\n}\n\n4. Run multiple pre-validation methods for updates\nfunction config() {\n    beforeValidationOnUpdate(\"updateTimestamp, sanitizeNotes\");\n}\n\nfunction updateTimestamp() {\n    this.lastModified = now();\n}\n\nfunction sanitizeNotes() {\n    this.notes = trim(this.notes);\n}
"},"hint":"Registers method(s) that should be called before an existing object is validated. This hook is useful when you want to adjust, sanitize, or enforce rules specifically for updates (not for new records). It ensures the object is in the correct state before validation checks run.\n\n","returntype":"void","slug":"model.beforeValidationOnUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeValidationOnUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Standard belongsTo association\n// Specify that instances of this model belong to an author\nbelongsTo(\"author\");\n\nWheels will automatically deduce the foreign key as authorId and the associated model as Author.\n\n2. Custom foreign key and model name\n// Foreign key does not follow convention\nbelongsTo(name = \"bookWriter\", modelName = \"author\", foreignKey = \"authorId\");\n\nUseful when your database column names or model names deviate from Wheels conventions.\n\n3. Specify LEFT OUTER JOIN\nbelongsTo(name = \"publisher\", joinType = \"outer\");
"},"hint":"Sets up a belongsTo association between this model and another model. Use this when the current model contains a foreign key referencing another model. This establishes a one-to-many relationship from the perspective of the other model (i.e., this model “belongs to” a parent model).\n\n","returntype":"void","slug":"model.belongsTo","parameters":[{"required":true,"hint":"Gives the association a name that you refer to when working with the association (in the `include` argument to `findAll`, to name one example).","name":"name","type":"string"},{"default":"","required":false,"hint":"Name of associated model (usually not needed if you follow Wheels conventions because the model name will be deduced from the `name` argument).","name":"modelName","type":"string"},{"default":"","required":false,"hint":"Foreign key property name (usually not needed if you follow Wheels conventions since the foreign key name will be deduced from the `name` argument).","name":"foreignKey","type":"string"},{"default":"","required":false,"hint":"Column name to join to if not the primary key (usually not needed if you follow Wheels conventions since the join key will be the table's primary key/keys).","name":"joinKey","type":"string"},{"default":"inner","required":false,"hint":"Use to set the join type when joining associated tables. Possible values are `inner` (for `INNER JOIN`) and `outer` (for `LEFT OUTER JOIN`).","name":"joinType","type":"string"}],"availableIn":["model"],"name":"belongsTo","tags":{"categoryClass":"associationfunctions","sectionClass":"modelconfiguration","category":"Association Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single big integer column\nbigInteger(columnNames=\"userId\");\n\n2. Add multiple big integer columns\nbigInteger(columnNames=\"orderId, invoiceId\");\n\n3. Add a column with a default value and disallow NULLs\nbigInteger(columnNames=\"views\", default=\"0\", allowNull=false);\n\n4. Add a column with a custom limit\nbigInteger(columnNames=\"serialNumber\", limit=20);
"},"hint":"Adds one or more big integer columns to a table definition in a migration. Use this when you need columns capable of storing large integer values, typically larger than standard integer columns.\n\n","returntype":"any","slug":"tabledefinition.bigInteger","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"numeric"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"bigInteger","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single binary column\nbinary(columnNames=\"profilePicture\");\n\n2. Add multiple binary columns\nbinary(columnNames=\"thumbnail, documentBlob\");\n\n3. Add a binary column that allows NULLs\nbinary(columnNames=\"attachment\", allowNull=true);\n\n4. Add a binary column with a default value\nbinary(columnNames=\"signature\", default=\"0x00\");
"},"hint":"Adds one or more binary columns to a table definition in a migration. Use this for storing raw binary data, such as files, images, or other byte streams.\n\n","returntype":"any","slug":"tabledefinition.binary","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"binary","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single boolean column\nboolean(columnNames=\"isActive\");\n\n2. Add multiple boolean columns\nboolean(columnNames=\"isPublished, isVerified\");\n\n3. Add a boolean column with a default value\nboolean(columnNames=\"isAdmin\", default=\"false\");\n\n4. Add a boolean column that allows NULLs\nboolean(columnNames=\"isArchived\", allowNull=true);
"},"hint":"Adds one or more boolean columns to a table definition in a migration. Use this for columns that store true/false values.\n\n","returntype":"any","slug":"tabledefinition.boolean","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"boolean","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic submit button\n#startFormTag(action=\"something\")#\n    #buttonTag(content=\"Submit this form\", value=\"save\")#\n#endFormTag()#\n\n2. Button with a different type\n#buttonTag(content=\"Reset form\", type=\"reset\")#\n\n3. Button using an image\n#buttonTag(image=\"submit.png\", value=\"save\")#\n\n4. Button with HTML wrappers\n#buttonTag(content=\"Click Me\", prepend=\"<div class='btn-wrapper'>\", append=\"</div>\")#\n\n5. Disable encoding for raw HTML content\n#buttonTag(content=\"<strong>Submit</strong>\", encode=false)#
"},"hint":"Builds and returns a string containing a button form control for use in your HTML forms. Use this helper to create buttons with custom content, types, values, images, and optional HTML wrappers.\n\n","returntype":"string","slug":"controller.buttonTag","parameters":[{"default":"Save changes","required":false,"hint":"Content to display inside the button.","name":"content","type":"string"},{"default":"submit","required":false,"hint":"The type for the button: `button`, `reset`, or `submit`.","name":"type","type":"string"},{"default":"save","required":false,"hint":"The value of the button when submitted.","name":"value","type":"string"},{"default":"","required":false,"hint":"File name of the image file to use in the button form control.","name":"image","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"buttonTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic button submitting to an action\n#buttonTo(text=\"Delete Account\", action=\"performDelete\", disable=\"Wait...\")#\n\n2. Button with an ID and class applied to the input\n#buttonTo(text=\"Edit\", action=\"edit\", inputId=\"edit-button\", inputClass=\"edit-button-class\")#\n\n3. Button using an image instead of text\n#buttonTo(image=\"delete-icon.png\", action=\"delete\")#\n\n4. Button linking to a specific route with query parameters\n#buttonTo(text=\"View Report\", route=\"reportRoute\", params=\"year=2025&month=9\")#\n\n5. Button using DELETE method\n#buttonTo(text=\"Remove\", action=\"deleteItem\", method=\"delete\")#
"},"hint":"Creates a form containing a single button that submits to a URL. The URL is constructed the same way as linkTo(). This helper is useful when you want a button that performs a specific action (GET, POST, PUT, DELETE, PATCH) without manually creating a form.\n\n","returntype":"string","slug":"controller.buttonTo","parameters":[{"default":"","required":false,"hint":"The text content of the button.","name":"text","type":"string"},{"default":"","required":false,"hint":"If you want to use an image for the button pass in the link to it here (relative from the `images` folder).","name":"image","type":"string"},{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: `wheels=cool&x=y`). Please note that Wheels uses the `&` and `=` characters to split the parameters and encode them properly for you. However, if you need to pass in `&` or `=` as part of the value, then you need to encode them (and only them), example: `a=cats%26dogs%3Dtrouble!&b=1`.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"required":false,"hint":"The type of `method` to use in the `form` tag (`delete`, `get`, `patch`, `post`, and `put` are the options).","name":"method","type":"string"},{"default":true,"required":false,"hint":"If `true`, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"buttonTo","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Cache a single action (default 60 minutes)\ncaches(\"termsOfUse\");\n\n2. Cache multiple actions for 30 minutes\ncaches(actions=\"browseByUser, browseByTitle\", time=30);\n\n3. Cache actions as static pages, skipping filters\ncaches(actions=\"termsOfUse, codeOfConduct\", static=true);\n\n4. Cache content separately based on runtime variable\ncaches(action=\"home\", appendToKey=\"request.region\");
"},"hint":"Tells Wheels to cache one or more controller actions. Caching improves performance by storing the output of actions so that repeated requests do not require re-running the action logic.\n\n","returntype":"void","slug":"controller.caches","parameters":[{"default":"","required":false,"hint":"Action(s) to cache. This argument is also aliased as `actions`.","name":"action","type":"string"},{"default":60,"required":false,"hint":"Minutes to cache the action(s) for.","name":"time","type":"numeric"},{"default":false,"required":false,"hint":"Set to `true` to tell Wheels that this is a static page and that it can skip running the controller filters (before and after filters set on actions). Please note that the `onSessionStart` and `onRequestStart` events still execute though.","name":"static","type":"boolean"},{"default":"","required":false,"hint":"List of variables to be evaluated at runtime and included in the cache key so that content can be cached separately.","name":"appendToKey","type":"string"}],"availableIn":["controller"],"name":"caches","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Clear a single action from cache\nclearCachableActions(\"termsOfUse\");\n\n2. Clear multiple actions from cache\nclearCachableActions(actions=\"termsOfUse,codeOfConduct\");\n\n3. Clear all cacheable actions in the controller\nclearCachableActions();
"},"hint":"Removes one or more actions from the list of cacheable actions in a controller. Use this when you want to prevent previously cached actions from being cached or to reset caching for certain actions.","returntype":"void","slug":"controller.clearCachableActions","parameters":[{"default":"","required":false,"hint":"Action(s) to remove from cache.","name":"action","type":"string"}],"availableIn":["controller"],"name":"clearCachableActions","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Capitalize a single sentence\n#capitalize(\"wheels is a framework\")#\n\n\n2. Capitalize a name\n#capitalize(\"john doe\")#\n\n\n3. Capitalize a title\n#capitalize(\"introduction to wheels framework\")#\n
"},"hint":"Capitalizes the first letter of every word in the provided text, creating a nicely formatted title or sentence.\n\n","returntype":"string","slug":"controller.capitalize","parameters":[{"required":true,"name":"text","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"capitalize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Alter a table to add new columns\nt = changeTable(name='employees');\nt.string(columnNames=\"fullName\", default=\"\", allowNull=true, limit=\"255\");\nt.change();\n\n
"},"hint":"Used in migrations to alter an existing table in the database. This function allows you to modify the structure of a table, such as adding, modifying, or removing columns.\n\n","returntype":"void","slug":"tabledefinition.change","parameters":[{"default":"false","required":false,"name":"addColumns","type":"boolean"}],"availableIn":["tabledefinition"],"name":"change","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Change the type and limit of a column\nchangeColumn(table='members', columnName='status', columnType='string', limit=50);\n\n2. Change a decimal column’s precision and scale\nchangeColumn(table='products', columnName='price', columnType='decimal', precision=10, scale=2);\n\n3. Change a column to allow NULL and set a default value\nchangeColumn(table='users', columnName='nickname', columnType='string', limit=100, allowNull=true, default='Guest');\n\n4. Move a column to a specific position in the table\nchangeColumn(table='orders', columnName='status', columnType='string', limit=20, afterColumn='orderDate');
"},"hint":"Changes the definition of an existing column in a database table. This function is used in migration CFCs to update column properties such as type, size, default value, nullability, precision, and scale.\n\n","returntype":"void","slug":"migration.changeColumn","parameters":[{"required":true,"hint":"The Name of the table where the column is","name":"table","type":"string"},{"required":true,"hint":"THe name of the column","name":"columnName","type":"string"},{"required":true,"hint":"The type of the column","name":"columnType","type":"string"},{"default":"","required":false,"hint":"The name of the column which this column should be inserted after","name":"afterColumn","type":"string"},{"default":"","required":false,"hint":"Name for reference column, see documentation for references function, required if columnType is 'reference'","name":"referenceName","type":"string"},{"required":false,"hint":"Default value for this column","name":"default","type":"string"},{"required":false,"hint":"Whether to allow NULL values","name":"allowNull","type":"boolean"},{"required":false,"hint":"Character or integer size limit for column","name":"limit","type":"numeric"},{"required":false,"hint":"(For decimal type) the maximum number of digits allow","name":"precision","type":"numeric"},{"required":false,"hint":"(For decimal type) the number of digits to the right of the decimal point","name":"scale","type":"numeric"},{"default":"false","required":false,"hint":"if true, attempts to add columns and database will likely throw an error if column already exists","name":"addColumns","type":"boolean"}],"availableIn":["migration"],"name":"changeColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Track changes on a single property\nmember = model(\"member\").findByKey(params.memberId);\nmember.email = params.newEmail;\n\n// Get the previous value of the email\noldValue = member.changedFrom(\"email\");\n\n2. Using dynamic property function\n// Dynamic method naming also works\noldValue = member.emailChangedFrom();\n\n3. Check before saving\nmember.firstName = \"Bruce\";\n\nif (member.changedFrom(\"firstName\") != \"\") {\n    writeOutput(\"First name was changed from \" & member.changedFrom(\"firstName\"));\n}\n\nmember.save();
"},"hint":"Returns the previous value of a property that has been modified on a model object. Wheels tracks changes to object properties until the object is saved to the database. If no previous value exists (the property was never modified), it returns an empty string. This is useful for auditing, logging, or conditional logic based on changes to object properties.\n\n","returntype":"string","slug":"model.changedFrom","parameters":[{"required":true,"hint":"Name of property to get the previous value for.","name":"property","type":"string"}],"availableIn":["model"],"name":"changedFrom","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Track changed properties\nmember = model(\"member\").findByKey(params.memberId);\nmember.firstName = params.newFirstName;\nmember.email = params.newEmail;\n\n// Get a list of properties that have changed\nchangedProperties = member.changedProperties();\n\n2. Conditional logic based on changes\nif (arrayLen(member.changedProperties()) > 0) {\n    writeOutput(\"The following fields were changed: \" & arrayToList(member.changedProperties()));\n}
"},"hint":"Returns a list of property names that have been modified on a model object but not yet saved to the database. This is useful for tracking which fields were updated, triggering specific actions based on changes, or performing conditional validation.\n\n","returntype":"string","slug":"model.changedProperties","parameters":[],"availableIn":["model"],"name":"changedProperties","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Add new columns to an existing table\nt = changeTable(name='employees');\nt.string(columnNames=\"fullName\", default=\"\", allowNull=true, limit=255);\nt.boolean(columnNames=\"isActive\", default=true);\nt.change();\n\n2. Modify multiple columns\nt = changeTable(name='products');\nt.string(columnNames=\"productName\", limit=150, allowNull=false);\nt.decimal(columnNames=\"price\", precision=10, scale=2);\nt.change();
"},"hint":"Creates a table definition object used to store and apply modifications to an existing table in the database. This function is only available inside a migration CFC and works in conjunction with table definition methods like string(), integer(), boolean(), etc., and the change() method to apply the changes.\n\n","returntype":"TableDefinition","slug":"migration.changeTable","parameters":[{"required":true,"hint":"Name of the table to set change properties on","name":"name","type":"string"}],"availableIn":["migration"],"name":"changeTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single CHAR column\nt.char(columnNames=\"status\", limit=1, default=\"A\", allowNull=false);\n\n2. Add multiple CHAR columns\nt.char(columnNames=\"type,code\", limit=2, default=\"\", allowNull=true);\n\n3. Add a CHAR column without a limit\nt.char(columnNames=\"initials\", allowNull=true);
"},"hint":"Adds one or more CHAR columns to a table definition in a migration. Use this function to define fixed-length string columns when creating or modifying a table.\n\n","returntype":"any","slug":"tabledefinition.char","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"any"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"char","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic checkbox for a single boolean property\n#checkBox(objectName="photo", property="isPublic", label="Display this photo publicly.")#\n\n2. Checkbox for a nested hasMany association\n<cfloop from="1" to="#ArrayLen(user.photos)#" index="i">\n    <div>\n        <h3>#user.photos[i].title#:</h3>\n        <div>\n            #checkBox(objectName="user", association="photos", position=i, property="isPublic", label="Display this photo publicly.")#\n        </div>\n    </div>\n</cfloop>
"},"hint":"Builds and returns a string containing a check box form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.checkBox","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":1,"required":false,"name":"checkedValue","type":"string"},{"default":0,"required":false,"hint":"The value of the check box when it's on the unchecked state.","name":"uncheckedValue","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"checkBox","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic checkbox\n#checkBoxTag(name="subscribe", value="true", label="Subscribe to our newsletter", checked=false)#\n\n2. Checkboxes generated from a query\n// Controller code\npizza = model("pizza").findByKey(session.pizzaId);\nselectedToppings = pizza.toppings();\ntoppings = model("topping").findAll(order="name");\n\n<!--- View code --->\n<fieldset>\n\t<legend>Toppings</legend>\n\t<cfoutput query="toppings">\n\t\t#checkBoxTag(name="toppings", value="true", label=toppings.name, checked=YesNoFormat(ListFind(ValueList(selectedToppings.id), toppings.id))#\n\t</cfoutput>\n</fieldset>
"},"hint":"Builds and returns a string containing a check box form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.checkBoxTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":false,"required":false,"hint":"Whether or not the check box should be checked by default.","name":"checked","type":"boolean"},{"default":1,"required":false,"hint":"Value of check box in its checked state.","name":"value","type":"string"},{"default":"","required":false,"hint":"The value of the check box when it's on the unchecked state.","name":"uncheckedValue","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"checkBoxTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Clear change information for a single property\n// Convert startTime to UTC in an \"afterFind\" callback\nthis.startTime = DateConvert(\"Local2UTC\", this.startTime);\n\n// Tell Wheels to clear internal change tracking for this property\nthis.clearChangeInformation(property=\"startTime\");\n\n2. Clear change information for all properties\n// Clear internal tracking for all properties of the object\nthis.clearChangeInformation();
"},"hint":"Clears all internal tracking information that Wheels maintains about an object’s properties. This does not undo changes made to the object—it simply resets the record of which properties are considered “changed,” so methods like hasChanged(), changedProperties(), or allChanges() will no longer report them. This is useful when you modify a property programmatically (for example, in a callback) and don’t want Wheels to attempt saving or reporting it as a change.\n\n","returntype":"void","slug":"model.clearChangeInformation","parameters":[{"required":false,"hint":"string false Name of property to clear information for.","name":"property","type":"string"}],"availableIn":["model"],"name":"clearChangeInformation","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Clear all errors on the object\n// Remove all errors regardless of property\nthis.clearErrors();\n\n2. Clear errors on a specific property\n// Remove all errors associated with the 'firstName' property\nthis.clearErrors(property=\"firstName\");\n\n3. Clear a specific error by name\n// Remove only the error named 'emailFormatError' without affecting other errors\nthis.clearErrors(name=\"emailFormatError\");
"},"hint":"Clears all validation or manual errors stored on a model object. You can clear all errors, or target specific errors either by property name or by a custom error name. This is useful when resetting an object’s state before re-validation, updating values programmatically, or handling conditional validation logic.\n\n","returntype":"void","slug":"model.clearErrors","parameters":[{"default":"","required":false,"hint":"Specify a property name here if you want to clear all errors set on that property.","name":"property","type":"string"},{"default":"","required":false,"hint":"Specify an error name here if you want to clear all errors set with that error name.","name":"name","type":"string"}],"availableIn":["model"],"name":"clearErrors","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
\n<cfscript>\n\nmapper()\n    // Create a route like `photos/search`\n    .resources(name="photos", nested=true)\n        .collection()\n            .get("search")\n        .end()\n    .end()\n.end();\n\n</cfscript>\n
"},"hint":"Defines a collection route in your Wheels application. Collection routes operate on a set of resources and do not require an id, unlike member routes which act on a single resource. This is useful when building actions that retrieve, filter, or display multiple objects, such as search pages, listings, or batch operations.\n\n","returntype":"struct","slug":"mapper.collection","parameters":[],"availableIn":["mapper"],"name":"collection","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a string column\nt = changeTable(name=\"employees\");\nt.column(columnName=\"fullName\", columnType=\"string\", limit=255, allowNull=false, default=\"Unknown\");\nt.change();\n\n2. Add a decimal column\nt = changeTable(name=\"products\");\nt.column(columnName=\"price\", columnType=\"decimal\", precision=10, scale=2, allowNull=false, default=\"0.00\");\nt.change();\n\n3. Add a boolean column\nt = changeTable(name=\"members\");\nt.column(columnName=\"isActive\", columnType=\"boolean\", allowNull=false, default=\"1\");\nt.change();
"},"hint":"Adds a column to a table definition in a migration. This function is used when defining or altering database tables. It supports multiple column types and allows you to specify constraints like default values, nullability, length, and precision. Use this inside a table definition object in a migration CFC when building or modifying tables.\n\n","returntype":"any","slug":"tabledefinition.column","parameters":[{"required":true,"name":"columnName","type":"string"},{"required":true,"name":"columnType","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"},{"required":false,"name":"limit","type":"any"},{"required":false,"name":"precision","type":"numeric"},{"required":false,"name":"scale","type":"numeric"}],"availableIn":["tabledefinition"],"name":"column","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Inspect a simple property\nuser = model(\"user\").columnDataForProperty(\"email\");\n\nwriteDump(user);\n\nOutput might include:\n{\n  \"column\": \"email\",\n  \"dataType\": \"string\",\n  \"columnDefault\": \"\",\n  \"nullable\": \"NO\",\n  \"size\": 255\n}\n\n2. Use column metadata for validation or dynamic forms\ncolumns = model(\"product\").columnDataForProperty(\"price\");\n\nif(columns.nullable EQ \"NO\" AND columns.dataType EQ \"decimal\") {\n    writeOutput(\"Price is required and must be decimal.\");\n}
"},"hint":"Returns a struct containing metadata about a specific property in a model. This includes information such as type, constraints, default values, and other column-specific details. It’s useful when you need to introspect the schema of your model dynamically.\n\n","returntype":"any","slug":"model.columnDataForProperty","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"columnDataForProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Retrieve the column name for a property\nuser = model(\"user\").columnForProperty(\"email\");\n\nwriteOutput(user);  // Might output: \"email_address\"\n\n2. Use in dynamic SQL queries\nuserModel = model(\"user\");\ncolumn = userModel.columnForProperty(\"firstName\");\nquery = \"SELECT #column# FROM users WHERE id = 1\";
"},"hint":"Returns the database column name that corresponds to a given model property. This is useful when your model property names differ from the actual database column names, or when you need to dynamically generate SQL queries or mappings.\n\n","returntype":"any","slug":"model.columnForProperty","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"columnForProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get all column names for a model\nuserModel = model(\"user\");\ncolumns = userModel.columnNames();\n\nwriteOutput(columns);\n// Might output: \"id,first_name,last_name,email,created_at,updated_at\"\n\n2. Use column names to dynamically select fields in a query\nuserModel = model(\"user\");\nqueryColumns = userModel.columnNames();\nq = \"SELECT #queryColumns# FROM users WHERE active = 1\";
"},"hint":"Returns a list of column names for the table mapped to this model. The list is ordered according to the columns’ ordinal positions in the database table. This is useful for dynamically generating queries, forms, or for inspecting the database structure associated with a model.\n\n","returntype":"string","slug":"model.columnNames","parameters":[],"availableIn":["model"],"name":"columnNames","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get an array of columns for a model\nuserModel = model(\"user\");\ncolumnArray = userModel.columns();\n\nwriteDump(columnArray);\n// Might output: [\"id\", \"first_name\", \"last_name\", \"email\", \"created_at\", \"updated_at\"]\n\n2. Loop through the columns for dynamic processing\nuserModel = model(\"user\");\nfor(column in userModel.columns()) {\n    writeOutput(\"Column: #column#
\");\n}
"},"hint":"Returns an array of database column names for the table associated with the model. This method excludes calculated or transient properties that are defined in the model but not stored in the database.\n\n","returntype":"array","slug":"model.columns","parameters":[],"availableIn":["model"],"name":"columns","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Compare two user objects\nuser1 = model(\"user\").findByKey(1);\nuser2 = model(\"user\").findByKey(2);\n\nif(user1.compareTo(user2)) {\n    writeOutput(\"Objects are the same.\");\n} else {\n    writeOutput(\"Objects are different.\");\n}\n\n2. Compare dynamically after changing a property\nuser1 = model(\"user\").findByKey(1);\nuser2 = model(\"user\").findByKey(1);\n\nuser2.email = \"[newemail@example.com](mailto:newemail@example.com)\";\n\nwriteDump(user1.compareTo(user2)); // Will output false because email changed
"},"hint":"Compares the current model object with another model object to determine if they are effectively the same. This is useful for checking equality between two instances of the same model before performing operations like updates or merges.\n\n","returntype":"boolean","slug":"model.compareTo","parameters":[{"required":true,"name":"object","type":"component"}],"availableIn":["model"],"name":"compareTo","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Constrain a route parameter to digits only\nmapper()\n    .resources(name=\"users\", nested=true)\n        .member(id=\":userId\")\n            .constraints({ userId=\"^\\d+$\" })\n        .end()\n    .end()\n.end();\n\nHere, the userId parameter must be a number, otherwise the route won’t match.\n\n2. Constrain multiple parameters\nmapper()\n    .resources(name=\"orders\", nested=true)\n        .member(orderId=\":orderId\", itemId=\":itemId\")\n            .constraints({ \n                orderId=\"^\\d+$\", \n                itemId=\"^\\d{3}-[A-Z]{2}$\" \n            })\n        .end()\n    .end()\n.end();
"},"hint":"Defines variable patterns for route parameters when setting up routes using the Wheels mapper(). This allows you to restrict the values that route parameters can take, such as limiting an id parameter to numbers only or enforcing a specific string format.\n\n","returntype":"struct","slug":"mapper.constraints","parameters":[],"availableIn":["mapper"],"name":"constraints","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\n<!--- In your view --->\n<cfsavecontent variable=\"mySidebar\">\n    <h1>My Sidebar Text</h1>\n</cfsavecontent>\n\n<cfset contentFor(sidebar=mySidebar)>\n\n<!--- In your layout --->\n<html>\n    <head><title>My Site</title></head>\n    <body>\n        <cfoutput>\n            #includeContent(\"sidebar\")#  <!-- Renders the sidebar content -->\n            #includeContent()#           <!-- Renders main content -->\n        </cfoutput>\n    </body>\n</html>\n\n2. Adding multiple pieces to the same section\n<cfset contentFor(sidebar=\"First piece of content\")>\n<cfset contentFor(sidebar=\"Second piece of content\", position=\"first\")>\n\n<!--- Renders 'Second piece of content' first, then 'First piece of content' -->\n#includeContent(\"sidebar\")#\n\n3. Overwriting content\n<cfset contentFor(sidebar=\"Old content\")>\n<cfset contentFor(sidebar=\"New content\", overwrite=true)>\n\n<!--- Only 'New content' will be rendered -->\n#includeContent(\"sidebar\")#
"},"hint":"contentFor() is used to store a section's output in a layout. It allows you to define content in your view templates and then render it in a layout using #includeContent()#. The function maintains a stack for each section, so multiple pieces of content can be added in a controlled order.\n\n","returntype":"void","slug":"controller.contentFor","parameters":[{"default":"last","required":false,"hint":"The position in the section's stack where you want the content placed. Valid values are `first`, `last`, or the numeric position.","name":"position","type":"any"},{"default":"false","required":false,"hint":"Whether or not to overwrite any of the content. Valid values are `false`, `true`, or `all`.","name":"overwrite","type":"any"}],"availableIn":["controller"],"name":"contentFor","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Controller:\n// PostsController.cfc\nfunction show() {\n    var post = model(\"post\").findByKey(params.id);\n}\n\nView (views/posts/show.cfm):\n<h2>#post.title#</h2>\n<p>#post.body#</p>\n\nLayout (views/layout.cfm):\n<html>\n<head>\n    <title>Blog</title>\n</head>\n<body>\n    <nav>Home | Posts</nav>\n\n    <!-- Inject view content -->\n    #contentForLayout()#\n\n    <footer>© 2025 My Blog</footer>\n</body>\n</html>\n\nOutput when visiting /posts/show?id=1:\n<html>\n<head>\n    <title>Blog</title>\n</head>\n<body>\n    <nav>Home | Posts</nav>\n\n    <h2>Hello World</h2>\n    <p>This is my first post!</p>\n\n    <footer>© 2025 My Blog</footer>\n</body>\n</html>
"},"hint":"contentForLayout() is used to render the main content of the current view inside a layout. In Wheels, when a controller action renders a view, that view generates content. This content can then be injected into the layout at the appropriate place using contentForLayout(). Essentially, it’s the placeholder for the view’s body content in your layout template.\n\n","returntype":"string","slug":"controller.contentForLayout","parameters":[],"availableIn":["controller"],"name":"contentForLayout","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":false,"docs":""},"hint":"The controller() function in Wheels is used to define routes that point to a specific controller. However, it is considered deprecated, because it does not align with RESTful routing principles. Wheels encourages using resources() and other RESTful routing helpers instead.\n\n","returntype":"struct","slug":"mapper.controller","parameters":[{"required":true,"name":"controller","type":"string"},{"default":"[runtime expression]","required":false,"name":"name","type":"string"},{"default":"[runtime expression]","required":false,"name":"path","type":"string"}],"availableIn":["mapper"],"name":"controller","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
testController = controller("users", params);
"},"hint":"The controller() function creates and returns a controller object with a custom name and optional parameters. It is primarily used for testing, but can also be used in code to instantiate a controller programmatically. Unlike the deprecated routing controller() function, this helper does not define routes—it creates controller instances.\n\n","returntype":"any","slug":"controller.controller","parameters":[{"required":true,"hint":"Name of the controller to create.","name":"name","type":"string"},{"default":"[runtime expression]","required":false,"hint":"The params struct (combination of form and URL variables).","name":"params","type":"struct"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"controller","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Count how many authors there are in the table\nauthorCount = model("author").count();\n\n2. Count how many authors that have a last name starting with an "A"\nauthorOnACount = model("author").count(where="lastName LIKE 'A%'");\n\n3. Count how many authors that have written books starting with an "A"\nauthorWithBooksOnACount = model("author").count(include="books", where="booktitle LIKE 'A%'");\n\n4. Count the number of comments on a specific post (a `hasMany` association from `post` to `comment` is required)\n// The `commentCount` method will call `model("comment").count(where="postId=#post.id#")` internally\naPost = model("post").findByKey(params.postId);\namount = aPost.commentCount();
"},"hint":"The count() method calculates the number of records in a table that match a given set of conditions. It internally uses the SQL COUNT() function. If no arguments are provided, it returns the total number of rows in the table. It works on model classes.\n\n","returntype":"any","slug":"model.count","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"count","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
t = table(name=\"employees\");\nt.string(columnNames=\"firstName\", limit=50, allowNull=false);\nt.string(columnNames=\"lastName\", limit=50, allowNull=false);\nt.integer(columnNames=\"age\", allowNull=true);\nt.boolean(columnNames=\"isActive\", default=\"1\");\n\n// Create the table in the database\nt.create();
"},"hint":"The create() method is used to create a database table based on the table definition that has been built using the migrator’s table definition functions (string(), integer(), boolean(), etc.). This method is only available within a migration CFC and finalizes the table creation in the database.\n\n","returntype":"void","slug":"tabledefinition.create","parameters":[],"availableIn":["tabledefinition"],"name":"create","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Create a new author and save it to the database\nnewAuthor = model("author").create(params.author);\n\n2. Same as above using named arguments\nnewAuthor = model("author").create(firstName="John", lastName="Doe");\n\n3. Same as above using both named arguments and a struct\nnewAuthor = model("author").create(active=1, properties=params.author);\n\n4. If you have a `hasOne` or `hasMany` association setup from `customer` to `order`, you can do a scoped call. (The `createOrder` method below will call `model("order").create(customerId=aCustomer.id, shipping=params.shipping)` internally.)\naCustomer = model("customer").findByKey(params.customerId);\nanOrder = aCustomer.createOrder(shipping=params.shipping);
"},"hint":"The create() method is used to instantiate a new model object, set its properties, and save it to the database (if validations pass). Even if validation fails, the method still returns the unsaved object, including any validation errors. It’s a higher-level convenience function that combines object creation, property assignment, validation, and saving into a single call. Property names and values can be passed in either using named arguments or as a struct to the properties argument.\n\n","returntype":"any","slug":"model.create","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to allow explicit assignment of `createdAt` or `updatedAt` properties","name":"allowExplicitTimestamps","type":"boolean"}],"availableIn":["model"],"name":"create","tags":{"categoryClass":"createfunctions","sectionClass":"modelclass","category":"Create Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Create an empty migration file:\n\nresult = application.wheels.migrator.createMigration(\"MyMigrationFile\");\n\n// Generates a blank migration file with a timestamped prefix.\n\n// You can then edit it to define your table or schema changes.\n\n2. Create a migration file from a template (e.g., create-table):\n\nresult = application.wheels.migrator.createMigration(\"MyMigrationFile\", \"create-table\");\n\n// Generates a migration file pre-populated with a create-table template.
"},"hint":"The createMigration() method is used to generate a new migration file for managing database schema changes. While you can call it from your application code, it is primarily intended for use via the CLI or Wheels GUI. A migration file allows you to define table creations, modifications, or deletions in a structured way that can be applied or rolled back consistently.\n\n","returntype":"string","slug":"migrator.createMigration","parameters":[{"required":true,"name":"migrationName","type":"string"},{"default":"","required":false,"name":"templateName","type":"string"},{"default":"timestamp","required":false,"name":"migrationPrefix","type":"string"}],"availableIn":["migrator"],"name":"createMigration","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
// Example: create a users table\nt = createTable(name='users'); \n\tt.string(columnNames='firstname,lastname', default='', allowNull=false, limit=50);\n\tt.string(columnNames='email', default='', allowNull=false, limit=255); \n\tt.string(columnNames='passwordHash', default='', allowNull=true, limit=500);\n\tt.string(columnNames='passwordResetToken,verificationToken', default='', allowNull=true, limit=500);\n\tt.boolean(columnNames='passwordChangeRequired,verified', default=false); \n\tt.datetime(columnNames='passwordResetTokenAt,passwordResetAt,loggedinAt', default='', allowNull=true); \n\tt.integer(columnNames='roleid', default=0, allowNull=false, limit=3);\n\tt.timestamps();\nt.create();\n\n// Example: Create a table with a different Primary Key\nt = createTable(name='tokens', id=false);\n\tt.primaryKey(name='id', allowNull=false, type="string", limit=35 );\n\tt.datetime(columnNames="expiresAt", allowNull=false);\n\tt.integer(columnNames='requests', default=0, allowNull=false);\n\tt.timestamps();\nt.create();\n\n// Example: Create a Join Table with composite primary keys\nt = createTable(name='userkintins', id=false); \n\tt.primaryKey(name="userid", allowNull=false, limit=11);\n\tt.primaryKey(name='profileid', type="string", limit=11 );  \nt.create();\n
"},"hint":"The createTable() function is used in migration CFCs to define a new database table. It returns a TableDefinition object, on which you can specify columns, primary keys, timestamps, and other table properties. Once the table is defined, you call create() to actually create it in the database.\n\n","returntype":"TableDefinition","slug":"migration.createTable","parameters":[{"required":true,"hint":"The name of the table to create","name":"name","type":"string"},{"default":"false","required":false,"hint":"whether to drop the table before creating it","name":"force","type":"boolean"},{"default":"true","required":false,"hint":"Whether to create a default primarykey or not","name":"id","type":"boolean"},{"default":"id","required":false,"hint":"Name of the primary key field to create","name":"primaryKey","type":"string"}],"availableIn":["migration"],"name":"createTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Simple View Creation\n\nv = createView(name='active_users');\nv.selectStatement(sql = \"SELECT * FROM c_o_r_e_users\")\nv.create();\n\n// Creates a view active_users that selects only active users from the users table.\n\n2. View with Join\n\nv = createView(name='user_orders');\nv.selectStatement(sql = \"SELECT u.id, u.firstname, u.lastname, o.id AS orderId, o.total FROM users u JOIN orders o ON u.id = o.userId WHERE o.status = \"completed\";\")\nv.create();\n\n// Creates a user_orders view joining users and orders tables, filtering only completed orders.
"},"hint":"The createView() function is used in migration CFCs to define a new database view. It returns a ViewDefinition object, on which you can specify the view’s SQL query and properties. Once the view is fully defined, you call create() to actually create it in the database.\n\n","returntype":"ViewDefinition","slug":"migration.createView","parameters":[{"required":true,"hint":"Name of the view to change properties on","name":"name","type":"string"}],"availableIn":["migration"],"name":"createView","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<head>\n    <title>My Application</title>\n    #csrfMetaTags()#\n</head>\n\n// This will output something like:\n// <meta name=\"csrf-token\" content=\"YOUR_AUTH_TOKEN_HERE\">\n// <meta name=\"csrf-param\" content=\"authenticityToken\">
"},"hint":"The csrfMetaTags() helper generates meta tags containing your application's CSRF authenticity token. This is useful for JavaScript/AJAX requests that need to POST data securely, ensuring that the request comes from a trusted source.\n\n","returntype":"string","slug":"controller.csrfMetaTags","parameters":[],"availableIn":["controller"],"name":"csrfMetaTags","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<!--- Alternating table row colors --->\n<table>\n\t<thead>\n\t\t<tr>\n\t\t\t<th>Name</th>\n\t\t\t<th>Phone</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t<cfoutput query="employees">\n\t\t\t<tr class="#cycle("odd,even")#">\n\t\t\t\t<td>#employees.name#</td>\n\t\t\t\t<td>#employees.phone#</td>\n\t\t\t</tr>\n\t\t</cfoutput>\n\t</tbody>\n</table>\n\n<!--- Alternating row colors and shrinking emphasis --->\n<cfoutput query="employees" group="departmentId">\n\t<div class="#cycle(values="even,odd", name="row")#">\n\t\t<ul>\n\t\t\t<cfoutput>\n\t\t\t\trank = cycle(values="president,vice-president,director,manager,specialist,intern", name="position")>\n\t\t\t\t<li class="#rank#">#categories.categoryName#</li>\n\t\t\t\tresetCycle("emphasis")>\n\t\t\t</cfoutput>\n\t\t</ul>\n\t</div>\n</cfoutput>
"},"hint":"cycle() is a view helper used to loop through a list of values sequentially, returning the next value each time it’s called. This is especially useful for things like alternating row colors in tables or assigning sequential classes in repeated HTML elements.\n\n","returntype":"string","slug":"controller.cycle","parameters":[{"required":true,"hint":"List of values to cycle through.","name":"values","type":"string"},{"default":"default","required":false,"hint":"Name to give the cycle. Useful when you use multiple cycles on a page.","name":"name","type":"string"}],"availableIn":["controller"],"name":"cycle","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
// In app/models/User.cfc\ncomponent extends=\"Model\" {\n\n    function config() {\n        // Use a custom datasource for this model\n        dataSource(\"users_source\");\n        \n        // Optional: specify credentials\n        // dataSource(\"users_source\", \"dbUser\", \"dbPass\");\n    }\n}
"},"hint":"dataSource() is a model configuration method used to override the default database connection for a specific model. This is useful when you want a model to query a different database or use specific credentials than the application default.\n\n","returntype":"void","slug":"model.dataSource","parameters":[{"required":true,"hint":"The data source name to connect to.","name":"datasource","type":"string"},{"default":"","required":false,"hint":"The username for the data source.","name":"username","type":"string"},{"default":"","required":false,"hint":"The password for the data source.","name":"password","type":"string"}],"availableIn":["model"],"name":"dataSource","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// In a migration CFC\nt = createTable(name=\"events\");\nt.date(columnNames=\"startDate,endDate\",  default=\"\",  allowNull=false);\nt.create();
"},"hint":"date() is a table definition function used in a migration CFC to add one or more DATE columns to a table.\n\n","returntype":"any","slug":"tabledefinition.date","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"date","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateSelect(objectName=\"user\", property=\"dateOfBirth\")#\n\nOutputs month/day/year selects for the user.dateOfBirth property.\n\n---\n\nExample 2: Only month and year (no day)\n#dateSelect(objectName=\"order\", property=\"expirationDate\", order=\"month,year\")#\n\nUseful for credit card expiration dates.\n\nOnly month and year dropdowns appear.\n\n---\n\nExample 3: Custom year range\n#dateSelect(objectName=\"event\", property=\"eventDate\", startYear=2000, endYear=2030)#\n\nDropdown shows years 2000–2030.\n\n---\n\nExample 4: Custom month display\n#dateSelect(objectName=\"user\", property=\"anniversary\", monthDisplay=\"abbreviations\")#\n\nMonths display as Jan, Feb, Mar… instead of full names.\n\n---\n\nExample 5: Include blank options\n#dateSelect(objectName=\"profile\", property=\"graduationDate\", includeBlank=\"- Select Date -\")#\n\nAdds a blank option at the top of each select with the label - Select Date -.\n\n---\n\nExample 6: Using labels and custom HTML\n#dateSelect(objectName=\"employee\", property=\"hireDate\", label=\"Hire Date\", labelPlacement=\"before\", prepend=\"<div class='date-wrapper'>\", append=\"</div>\")#\n\nAdds a label and wraps selects inside a div for styling.
"},"hint":"Builds and returns a string containing three select form controls for month, day, and year based on the supplied objectName and property.\n\n","returntype":"string","slug":"controller.dateSelect","parameters":[{"default":"","required":false,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"default":"","required":false,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a `hasMany` relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date `select` tags.","name":"order","type":"string"},{"default":" ","required":false,"name":"separator","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"name":"monthAbbreviations","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the `select` form control. Pass `true` to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using `aroundLeft` or `aroundRight`.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The `class` name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"dateSelect","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateSelectTags(name=\"dateStart\", selected=params.dateStart)#\n\nOutputs month/day/year selects with the value pre-selected from params.dateStart.\n\n---\n\nExample 2: Month and year only\n#dateSelectTags(name=\"expiration\", selected=params.expiration, order=\"month,year\")#\n\nUseful for credit card expiration date inputs.\n\nOnly month and year dropdowns appear.\n\n---\n\nExample 3: Custom year range\n#dateSelectTags(name=\"eventDate\", startYear=2000, endYear=2030)#\n\nDropdown shows years 2000–2030.\n\n---\n\nExample 4: Custom month display\n#dateSelectTags(name=\"anniversary\", monthDisplay=\"abbreviations\")#\n\nMonths display as Jan, Feb, Mar… instead of full names.\n\n---\n\nExample 5: Include blank options\n#dateSelectTags(name=\"graduationDate\", includeBlank=\"- Select Date -\")#\n\nAdds a blank option at the top of each dropdown with - Select Date -.\n\n---\n\nExample 6: Using labels and custom HTML\n#dateSelectTags(name=\"hireDate\", label=\"Hire Date\", labelPlacement=\"before\", prepend=\"<div class='date-wrapper'>\", append=\"</div>\")#\n\nAdds a label and wraps selects inside a <div> for styling.
"},"hint":"dateSelectTags() is similar to dateSelect(), but instead of binding to a model object, it works directly with a name and selected value. It generates three select dropdowns (month, day, year) for form tags.\n\n","returntype":"string","slug":"controller.dateSelectTags","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date `select` tags.","name":"order","type":"string"},{"default":" ","required":false,"hint":"[see:dateSelect].","name":"separator","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"hint":"[see:dateSelect].","name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"hint":"[see:dateSelect].","name":"monthAbbreviations","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"dateSelectTags","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\nt = createTable(name=\"appointments\"); \nt.datetime(columnNames=\"startAt,endAt\");\nt.create();\n\nCreates startAt and endAt columns as datetime columns in the appointments table.\n\n---\n\nExample 2: With NULL allowed\nt = createTable(name=\"events\"); \nt.datetime(columnNames=\"cancelledAt\", allowNull=true);\nt.create();\n\ncancelledAt column allows NULL values.\n\n---\n\nExample 3: With default timestamp\nt = createTable(name=\"logs\"); \nt.datetime(columnNames=\"createdAt\", default=\"CURRENT_TIMESTAMP\");\nt.create();\n\nSets createdAt to the current timestamp by default.\n\n---\n\nExample 4: Multiple datetime columns with defaults\nt = createTable(name=\"tasks\"); \nt.datetime(columnNames=\"assignedAt,completedAt\", default=\"CURRENT_TIMESTAMP\", allowNull=false);\nt.create();\n\nBoth columns are non-nullable and default to the current timestamp.
"},"hint":"Adds datetime columns to a table definition when creating or altering a table in a migration. These columns store both date and time values.\n\n","returntype":"any","slug":"tabledefinition.datetime","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"datetime","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateTimeSelect(objectName=\"article\", property=\"publishedAt\")#\n\nGenerates all six selects for article.publishedAt.\n\n---\n\nExample 2: Custom date and time order\n#dateTimeSelect(objectName=\"appointment\", property=\"dateTimeStart\", dateOrder=\"month,day\", timeOrder=\"hour,minute\")#\n\nOnly shows month & day for date and hour & minute for time.\n\n---\n\nExample 3: 12-hour format with AM/PM\n#dateTimeSelect(objectName=\"meeting\", property=\"startTime\", twelveHour=true, timeOrder=\"hour,minute\")#\n\nHours dropdown uses 1–12 with AM/PM options.\n\n---\n\nExample 4: Include blank options and custom year range\n#dateTimeSelect(objectName=\"event\", property=\"eventTime\", startYear=2020, endYear=2030, includeBlank=true)#\n\nAdds an empty option for each select and sets the year range to 2020–2030.\n\n---\n\nExample 5: Custom separators\n#dateTimeSelect(objectName=\"flight\", property=\"departure\", dateSeparator=\"/\", timeSeparator=\".\")#\n\nShows / between date selects and . between time selects.
"},"hint":"Builds and returns a string containing six select form controls (three for date selection and the remaining three for time selection) based on the supplied objectName and property.\n\n","returntype":"string","slug":"controller.dateTimeSelect","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"string"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date select tags.","name":"dateOrder","type":"string"},{"default":" ","required":false,"name":"dateSeparator","type":"string"},{"default":2018,"required":false,"hint":"Last year in select list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"name":"monthAbbreviations","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"timeOrder","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"timeSeparator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc","name":"secondStep","type":"numeric"},{"default":" - ","required":false,"hint":"Use to change the character that is displayed between the first and second set of select tags.","name":"separator","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"Whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"dateTimeSelect","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateTimeSelectTags(\n    name=\"dateTimeStart\",\n    selected=params.dateTimeStart\n)#\n\nGenerates six selects for date/time with default order and all fields included.\n\n---\n\nExample 2: Show only month, day, hour, and minute\n#dateTimeSelectTags(\n    name=\"dateTimeStart\",\n    selected=params.dateTimeStart,\n    dateOrder=\"month,day\",\n    timeOrder=\"hour,minute\"\n)#\n\nExcludes year and seconds from the dropdowns.\n\n---\n\nExample 3: 12-hour format with AM/PM\n#dateTimeSelectTags(\n    name=\"meetingTime\",\n    selected=params.meetingTime,\n    twelveHour=true,\n    timeOrder=\"hour,minute\"\n)#\n\nHours are displayed as 1–12 with AM/PM dropdown.\n\n---\n\nExample 4: Custom year range with blank options\n#dateTimeSelectTags(\n    name=\"eventTime\",\n    selected=params.eventTime,\n    startYear=2020,\n    endYear=2030,\n    includeBlank=true\n)#\n\nAdds blank options and limits year selection to 2020–2030.\n\n---\n\nExample 5: Custom separators between date and time\n#dateTimeSelectTags(\n    name=\"flightDeparture\",\n    selected=params.departure,\n    dateSeparator=\"/\",\n    timeSeparator=\".\"\n)#\n\nUses / between date selects and . between time selects.
"},"hint":"Builds and returns a string containing six select form controls (three for date selection and the remaining three for time selection) based on a name.\n\n","returntype":"string","slug":"controller.dateTimeSelectTags","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date select tags.","name":"dateOrder","type":"string"},{"default":" ","required":false,"hint":"[see:dateTimeSelect].","name":"dateSeparator","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"hint":"[see:dateSelect].","name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"hint":"[see:dateSelect].","name":"monthAbbreviations","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"timeOrder","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"timeSeparator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":" - ","required":false,"hint":"Use to change the character that is displayed between the first and second set of select tags.","name":"separator","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"dateTimeSelectTags","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#daySelectTag(name=\"dayOfWeek\", selected=params.dayOfWeek)#\n\nGenerates a standard select dropdown for all days of the week.\n\nPre-selects the value from params.dayOfWeek if available.\n\n---\n\nExample 2: Include a blank option\n#daySelectTag(name=\"meetingDay\", selected=params.meetingDay, includeBlank=true)#\n\nAdds a blank option at the top so users can select nothing.\n\n---\n\nExample 3: Custom label before the field\n#daySelectTag(\n    name=\"deliveryDay\",\n    selected=params.deliveryDay,\n    label=\"Choose delivery day:\",\n    labelPlacement=\"before\"\n)#\n\nAdds a label that appears before the dropdown.\n\n---\n\nExample 4: Prepend and append HTML\n#daySelectTag(\n    name=\"eventDay\",\n    prepend=\"<div class='select-wrapper'>\",\n    append=\"</div>\"\n)#\n\nWraps the dropdown inside a <div> for styling purposes.
"},"hint":"Builds and returns a string containing a select form control for the days of the week based on the supplied name. This version works without binding to a model object.\n\n","returntype":"string","slug":"controller.daySelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"daySelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n// In a test\nuser = model(\"user\").findByKey(1);\n\n// Inspect the user object\ndebug(user);\n\nDumps the contents of the user object to the test output.\n\n---\n\nExample 2: Debug without output\n// Evaluate an expression but don't output\nresult = someFunction();\ndebug(result, display=false);\n\nUseful when you want to leave the debug call in place for later but don’t want it to show in test output immediately.\n\n---\n\nExample 3: Debug an expression directly\ndebug(\"2 + 2\");\n\nQuickly examines a simple expression, like a calculation or string.
"},"hint":"Used in tests to inspect any expression. It behaves like a cfdump but is tailored for the testing environment. This helps you examine values while writing or running legacy tests.\n\n","returntype":"any","slug":"test.debug","parameters":[{"required":true,"hint":"The expression to examine","name":"expression","type":"string"},{"default":true,"required":false,"hint":"Whether to display the debug call. False returns without outputting anything into the buffer. Good when you want to leave the debug command in the test for later purposes, but don't want it to display","name":"display","type":"boolean"}],"availableIn":["test"],"name":"debug","tags":{"categoryClass":"testingfunctions","sectionClass":"testmodel","category":"Testing Functions","section":"Test Model"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic decimal column\nt = changeTable(\"products\");\nt.decimal(columnNames=\"price\", default=\"0.00\", allowNull=false, precision=10, scale=2);\nt.change();\n\nAdds a price column with up to 10 digits, 2 of which are after the decimal point, default 0.00, and cannot be NULL.\n\n---\n\nExample 2: Multiple decimal columns\nt = changeTable(\"invoices\");\nt.decimal(columnNames=\"tax,discount\", default=\"0.00\", allowNull=false, precision=8, scale=2);\nt.change();\n\nAdds tax and discount columns with the same configuration.\n\n---\n\nExample 3: Nullable decimal column with no default\nt = createTable(\"payments\");\nt.decimal(columnNames=\"amountDue\", allowNull=true, precision=12, scale=4);\nt.create();\n\nAdds a amountDue column that can be NULL and allows up to 12 digits, 4 of which are after the decimal.
"},"hint":"Adds decimal (numeric) columns to a table definition when creating or altering tables via a migration CFC.\n\n","returntype":"any","slug":"tabledefinition.decimal","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"},{"required":false,"name":"precision","type":"numeric"},{"required":false,"name":"scale","type":"numeric"}],"availableIn":["tabledefinition"],"name":"decimal","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  articleReview\n    // Example URL: /articles/987/reviews/12542\n    // Controller:  Reviews\n    // Action:      delete\n    .delete(name="articleReview", pattern="articles/[articleKey]/reviews/[key]", to="reviews##delete")\n\n    // Route name:  cookedBooks\n    // Example URL: /cooked-books\n    // Controller:  CookedBooks\n    // Action:      delete\n    .delete(name="cookedBooks", controller="cookedBooks", action="delete")\n\n    // Route name:  logout\n    // Example URL: /logout\n    // Controller:  Sessions\n    // Action:      delete\n    .delete(name="logout", to="sessions##delete")\n\n    // Route name:  clientsStatus\n    // Example URL: /statuses/4918\n    // Controller:  clients.Statuses\n    // Action:      delete\n    .delete(name="statuses", to="statuses##delete", package="clients")\n\n    // Route name:  blogComment\n    // Example URL: /comments/5432\n    // Controller:  blog.Comments\n    // Action:      delete\n    .delete(\n        name="comment",\n        pattern="comments/[key]",\n        to="comments##delete",\n        package="blog"\n    )\n.end();\n\n</cfscript>\n
"},"hint":"Create a route that matches a URL requiring an HTTP DELETE method. We recommend using this matcher to expose actions that delete database records.\n\n","returntype":"struct","slug":"mapper.delete","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"delete","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete a single object\n<cfscript>\npost = model(\"post\").findByKey(33);\nsuccess = post.delete();\n</cfscript>\n\nDeletes the post with ID 33 from the database.\n\nReturns true if deletion succeeds.\n\nExample 2: Scoped delete via association\n<cfscript>\npost = model(\"post\").findByKey(params.postId);\ncomment = model(\"comment\").findByKey(params.commentId);\n\n// Calls comment.delete() internally\npost.deleteComment(comment);\n</cfscript>\n\nIf post has a hasMany association to comment, this uses the association method to delete a related comment.\n\nExample 3: Permanent deletion (bypass soft-delete)\n<cfscript>\npost = model(\"post\").findByKey(33);\npost.delete(softDelete=false);\n</cfscript>\n\nForces a hard delete even if the model uses soft-delete columns.
"},"hint":"Deletes the object, which means the row is deleted from the database (unless prevented by a beforeDelete callback).\nReturns true on successful deletion of the row, false otherwise.\n\n","returntype":"boolean","slug":"model.delete","parameters":[{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"delete","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete inactive users (skip callbacks and validations)\n<cfscript>\nrecordsDeleted = model(\"user\").deleteAll(where=\"inactive=1\", instantiate=false);\nwriteOutput(\"Deleted #recordsDeleted# inactive users.\");\n</cfscript>\n\nDeletes all users where inactive=1.\n\nObjects are not instantiated, so callbacks and validations are skipped.\n\nExample 2: Scoped delete using an association\n<cfscript>\npost = model(\"post\").findByKey(params.postId);\n\n// Deletes all comments associated with this post\nhowManyDeleted = post.deleteAllComments();\nwriteOutput(\"Deleted #howManyDeleted# comments for this post.\");\n</cfscript>\n\nAssumes a hasMany association from post → comment.\n\nInternally calls model(\"comment\").deleteAll(where=\"postId=#post.id#\").\n\nExample 3: Delete and run callbacks\n<cfscript>\nrecordsDeleted = model(\"user\").deleteAll(where=\"inactive=1\", instantiate=true, callbacks=true);\n</cfscript>\n\nDeletes the records after instantiating the objects.\n\nAny beforeDelete or afterDelete callbacks are triggered.
"},"hint":"Deletes all records that match the where argument.\nBy default, objects will not be instantiated and therefore callbacks and validations are not invoked.\nYou can change this behavior by passing in instantiate=true.\nReturns the number of records that were deleted.\n\n","returntype":"numeric","slug":"model.deleteAll","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Whether or not to instantiate the object(s) first. When objects are not instantiated, any callbacks and validations set on them will be skipped.","name":"instantiate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"deleteAll","tags":{"categoryClass":"deletefunctions","sectionClass":"modelclass","category":"Delete Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete a user by primary key\n<cfscript>\nresult = model(\"user\").deleteByKey(1);\n\nif (result) {\n    writeOutput(\"User deleted successfully.\");\n} else {\n    writeOutput(\"Failed to delete user.\");\n}\n</cfscript>\n\nDeletes the user with id=1.\n\nReturns true on success, false on failure.\n\nExample 2: Delete a record permanently (ignore soft delete)\n<cfscript>\nresult = model(\"user\").deleteByKey(1, softDelete=false);\n\nif (result) {\n    writeOutput(\"User permanently deleted.\");\n}\n</cfscript>\n\nIgnores any soft delete column.\n\nThe record is removed from the database entirely.\n\nExample 3: Disable callbacks\n<cfscript>\nresult = model(\"user\").deleteByKey(1, callbacks=false);\n\nwriteOutput(\"Deleted user without triggering callbacks: #result#\");\n</cfscript>\n\nSkips any beforeDelete or afterDelete logic.
"},"hint":"Finds the record with the supplied key and deletes it.\nReturns true on successful deletion of the row, false otherwise.\n\n","returntype":"boolean","slug":"model.deleteByKey","parameters":[{"required":true,"hint":"Primary key value(s) of the record to fetch. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"deleteByKey","tags":{"categoryClass":"deletefunctions","sectionClass":"modelclass","category":"Delete Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete the most recently signed-up user\n<cfscript>\nresult = model(\"user\").deleteOne(order=\"signupDate DESC\");\n\nif (result) {\n    writeOutput(\"Deleted the most recently signed-up user.\");\n} else {\n    writeOutput(\"No user found to delete.\");\n}\n</cfscript>\n\nDeletes one record based on the order of signupDate descending.\n\nOnly the first matching record is deleted.\n\nExample 2: Delete a specific user by condition\n<cfscript>\nresult = model(\"user\").deleteOne(where=\"email='test@example.com'\");\n\nwriteOutput(\"Deletion status: #result#\");\n</cfscript>\n\nFinds a user with the email test@example.com and deletes it.\n\nExample 3: Scoped delete via association\n<cfscript>\n// Assuming a hasOne association: user -> profile\naUser = model(\"user\").findByKey(params.userId);\naUser.deleteProfile(); // deletes the profile associated with this user\n</cfscript>\n\ndeleteProfile() internally calls model(\"profile\").deleteOne(where=\"userId=#aUser.id#\").\n
"},"hint":"Finds a single record based on conditions and deletes it. Returns true if deletion succeeds, false otherwise. It is useful when you want to remove one specific record without fetching it manually first.\n\n","returntype":"boolean","slug":"model.deleteOne","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"deleteOne","tags":{"categoryClass":"deletefunctions","sectionClass":"modelclass","category":"Delete Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Deobfuscate a single value\n<cfscript>\n// Assume \"b7ab9a50\" is an obfuscated ID\noriginalValue = deobfuscateParam(\"b7ab9a50\");\n\nwriteOutput(\"Original value: #originalValue#\");\n</cfscript>\n\nConverts the obfuscated string \"b7ab9a50\" back to its original value.\n\nUseful for safely passing IDs in URLs or forms while preventing direct exposure of database keys.\n\nExample 2: Deobfuscate a request parameter\n<cfscript>\n// Assume params.userId contains an obfuscated user ID\nuserId = deobfuscateParam(params.userId);\n\nuser = model(\"user\").findByKey(userId);\nwriteDump(user);\n</cfscript>\n\nSafely retrieves a user using an obfuscated ID passed in a URL or form.
"},"hint":"Converts an obfuscated string back into its original value. This is typically used when IDs or other sensitive data are encoded for security purposes and need to be restored to their original form.\n\n","returntype":"string","slug":"controller.deobfuscateParam","parameters":[{"required":true,"hint":"The value to deobfuscate.","name":"param","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"deobfuscateParam","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n<cfscript>\nrightNow = now();\naWhileAgo = dateAdd(\"d\", -30, rightNow);\n\ntimeDifference = distanceOfTimeInWords(aWhileAgo, rightNow);\nwriteOutput(timeDifference); // Outputs: \"about 1 month\"\n</cfscript>\n\nCalculates the difference between two dates.\n\nReturns \"about 1 month\" because aWhileAgo is 30 days before rightNow.\n\nExample 2: Include seconds\n<cfscript>\nstartTime = now();\nendTime = dateAdd(\"s\", 45, startTime);\n\ntimeDifference = distanceOfTimeInWords(startTime, endTime, true);\nwriteOutput(timeDifference); // Outputs: \"less than a minute\" or \"45 seconds\" depending on Wheels version\n</cfscript>\n\nUseful when you need a more precise human-readable difference for very short intervals.\n\nExample 3: Past vs future dates\n<cfscript>\npastDate = dateAdd(\"d\", -10, now());\nfutureDate = dateAdd(\"d\", 5, now());\n\nwriteOutput(distanceOfTimeInWords(pastDate, now()));   // \"10 days\"\nwriteOutput(distanceOfTimeInWords(now(), futureDate)); // \"5 days\"\n</cfscript>\n\nWorks regardless of the order of the dates.\n\nAlways returns a human-friendly description.
"},"hint":"Pass in two dates to this method, and it will return a string describing the difference between them.\n\n","returntype":"string","slug":"controller.distanceOfTimeInWords","parameters":[{"required":true,"hint":"Date to compare from.","name":"fromTime","type":"date"},{"required":true,"hint":"Date to compare to.","name":"toTime","type":"date"},{"default":false,"required":false,"hint":"Whether or not to include the number of seconds in the returned string.","name":"includeSeconds","type":"boolean"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"distanceOfTimeInWords","tags":{"categoryClass":"datefunctions","sectionClass":"globalhelpers","category":"Date Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
function down() {\n\ttransaction {\n\t\ttry {\n\t\t\t// your code goes here\n\t\t\tdropTable('myTable');\n\t\t} catch (any e) {\n\t\t\tlocal.exception = e;\n\t\t}\n\n\t\tif (StructKeyExists(local, "exception")) {\n\t\t\ttransaction action="rollback";\n\t\t\tthrow(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any");\n\t\t} else {\n\t\t\ttransaction action="commit";\n\t\t}\n\t}\n}\n
"},"hint":"down() defines the steps to revert a database migration. It’s executed when rolling back a migration, typically to undo the changes applied by the corresponding up() function. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.down","parameters":[],"availableIn":["migration"],"name":"down","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function up() {\n    // Remove a foreign key from the orders table\n    dropForeignKey(\n        table=\"orders\",\n        keyName=\"fk_orders_customerId\"\n    );\n}\n\ntable = \"orders\" -> the table that has the foreign key.\n\nkeyName = \"fk_orders_customerId\" -> the exact name of the foreign key constraint you want to drop.
"},"hint":"dropForeignKey() is used to remove a foreign key constraint from a table in the database. This is typically done during schema changes in migrations. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.dropForeignKey","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"the name of the key to drop","name":"keyName","type":"string"}],"availableIn":["migration"],"name":"dropForeignKey","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function up() {\n    // Remove a foreign key reference from the orders table\n    dropReference(\n        table=\"orders\",\n        referenceName=\"customer_ref\"\n    );\n}\n\ntable = \"orders\" -> the table that contains the foreign key reference.\n\nreferenceName = \"customer_ref\" -> the reference name that was originally defined when the foreign key was created.
"},"hint":"dropReference() is used to remove a foreign key constraint from a table in the database using the reference name that was originally used to create it. This is slightly different from dropForeignKey(), which requires the actual key name. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.dropReference","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"the name of the reference to drop","name":"referenceName","type":"string"}],"availableIn":["migration"],"name":"dropReference","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function down() {\n    // Drop the 'users' table\n    dropTable(name=\"users\");\n}\n\nname = \"users\" -> the table that you want to remove from the database.\n\nNotes\n\nTypically used in the down() method of a migration when rolling back a previous createTable().\n\nCan be combined with transaction {} to ensure rollback in case of errors:\n\nfunction down() {\n    transaction {\n        try {\n            dropTable(\"orders\");\n        } catch (any e) {\n            transaction action=\"rollback\";\n            throw(errorCode=\"1\", detail=e.detail, message=e.message, type=\"any\");\n        }\n        transaction action=\"commit\";\n    }\n}\n\nCaution: This operation permanently deletes all data in the table.
"},"hint":"dropTable() is used to remove a table from the database entirely. This is a destructive operation, so all data in the table will be lost. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.dropTable","parameters":[{"required":true,"hint":"Name of the table to drop","name":"name","type":"string"}],"availableIn":["migration"],"name":"dropTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function down() {\n    // Drop the 'active_users' view\n    dropView(name=\"active_users\");\n}\n\nname = \"active_users\" -> the view that you want to remove from the database.\n\nNotes\n\nTypically used in the down() method of a migration when rolling back a previous createView().\n\nCan be wrapped in a transaction for safety:\n\nfunction down() {\n    transaction {\n        try {\n            dropView(\"recent_orders\");\n        } catch (any e) {\n            transaction action=\"rollback\";\n            throw(errorCode=\"1\", detail=e.detail, message=e.message, type=\"any\");\n        }\n        transaction action=\"commit\";\n    }\n}\n\nCaution: This permanently deletes the view definition. Any queries depending on the view will fail after this operation.
"},"hint":"dropView() is used to remove a database view entirely. A view is a saved query that acts like a virtual table, so this operation deletes that virtual table definition. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.dropView","parameters":[{"required":true,"hint":"Name of the view to drop","name":"name","type":"string"}],"availableIn":["migration"],"name":"dropView","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    .namespace("admin")\n        .resources("products")\n    .end() // Ends the `namespace` block.\n\n    .scope(package="public")\n        .resources(name="products", nested=true)\n          .resources("variations")\n        .end() // Ends the nested `resources` block.\n    .end() // Ends the `scope` block.\n.end(); // Ends the `mapper` block.\n\n</cfscript>
"},"hint":"Call this to end a nested routing block or the entire route configuration. This method is chained on a sequence of routing mapper method calls started by mapper().\n\n","returntype":"struct","slug":"mapper.end","parameters":[],"availableIn":["mapper"],"name":"end","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
#startFormTag(action="create")#\n <input type="text" name="firstName" placeholder="First Name">\n <input type="text" name="lastName" placeholder="Last Name">\n#endFormTag()#\n\nOutput:\n<form action="/create" method="post">\n <input type="text" name="firstName" placeholder="First Name">\n <input type="text" name="lastName" placeholder="Last Name">\n</form>\n\n#startFormTag(action="update")#\n <input type="email" name="email" placeholder="Email">\n#endFormTag(prepend="<div class='form-wrapper'>", append="</div>")#\n\nOutput:\n<div class='form-wrapper'>\n<form action="/update" method="post">\n <input type="email" name="email" placeholder="Email">\n</form>\n</div>\n\n#startFormTag(action="login")#\n <input type="text" name="username">\n#endFormTag(encode=true)#\n\nOutput:\n<form action="/login" method="post">\n <input type="text" name="username">\n</form>\n\n#startFormTag(action="register", prepend="<section>")#\n <input type="text" name="username">\n <input type="password" name="password">\n#endFormTag(append="</section>")#\n\nOutput:\n<section>\n<form action="/register" method="post">\n <input type="text" name="username">\n <input type="password" name="password">\n</form>\n</section>\n
"},"hint":"Builds and returns a string containing the closing form tag. It’s typically used in conjunction with startFormTag().\n\n","returntype":"string","slug":"controller.endFormTag","parameters":[{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"endFormTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Check how many errors are set on the object\nif(author.errorCount() GTE 10){\n    // Do something to deal with this very erroneous author here...\n}\n\n2. Check how many errors are associated with the `email` property\nif(author.errorCount("email") gt 0){\n    // Do something to deal with this erroneous author here...\n}\n\n3. Count errors by error name\nif (author.errorCount(\"\", \"invalidFormat\") GT 0) {\n    // Handle errors with a specific error name\n    writeOutput(\"There are fields with invalid formatting!\");\n}
"},"hint":"Returns the number of errors this object has associated with it.\nSpecify property or name if you wish to count only specific errors.\n\n","returntype":"numeric","slug":"model.errorCount","parameters":[{"default":"","required":false,"hint":"Specify a property name here if you want to count only errors set on a specific property.","name":"property","type":"string"},{"default":"","required":false,"hint":"Specify an error name here if you want to count only errors set with a specific error name.","name":"name","type":"string"}],"availableIn":["model"],"name":"errorCount","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfoutput>\n#errorMessageOn(objectName="author", property="email")#\n</cfoutput>\n\nDisplays the first error message for the email property of the author object.\n\nDefault wrapper is <span class="error-message">.\n\nExample 2 — Custom wrapper and class\n<cfoutput>\n#errorMessageOn(\n objectName="author",\n property="email",\n wrapperElement="div",\n class="alert alert-danger"\n)#\n</cfoutput>\n\nWraps the error in a <div> instead of <span>.\n\nUses Bootstrap classes for styling.\n\nExample 3 — Prepend or append text\n<cfoutput>\n#errorMessageOn(\n objectName="author",\n property="email",\n prependText="Error: ",\n appendText=" Please fix it."\n)#\n</cfoutput>\n\nPrepends "Error: " and appends " Please fix it." around the actual error message.\n\nExample 4 — With HTML encoding disabled\n<cfoutput>\n#errorMessageOn(\n objectName="author",\n property="email",\n encode=false\n)#\n</cfoutput>\n\nOutput is not encoded, which can be useful if you want to include HTML formatting inside the error message itself.\n
"},"hint":"Returns the error message, if one exists, on the object's property.\nIf multiple error messages exist, the first one is returned. If no error exists, it returns an empty string.\n\n","returntype":"string","slug":"controller.errorMessageOn","parameters":[{"required":true,"hint":"The variable name of the object to display the error message for.","name":"objectName","type":"string"},{"required":true,"hint":"The name of the property to display the error message for.","name":"property","type":"string"},{"default":"","required":false,"hint":"String to prepend to the error message.","name":"prependText","type":"string"},{"default":"","required":false,"hint":"String to append to the error message.","name":"appendText","type":"string"},{"default":"span","required":false,"hint":"HTML element to wrap the error message in.","name":"wrapperElement","type":"string"},{"default":"error-message","required":false,"hint":"CSS `class` to set on the wrapper element.","name":"class","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"errorMessageOn","tags":{"categoryClass":"errorfunctions","sectionClass":"viewhelpers","category":"Error Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfoutput>\n#errorMessagesFor(objectName="author")#\n</cfoutput>\n\nGenerates a <ul class="error-messages"> containing all errors for the author object.\n\nDefault behavior includes all associated object errors.\n\nExample 2 — Custom CSS class\n<cfoutput>\n#errorMessagesFor(objectName="author", class="alert alert-danger")#\n</cfoutput>\n\nUses a custom CSS class for styling (e.g., Bootstrap alerts).\n\nExample 3 — Exclude duplicate errors\n<cfoutput>\n#errorMessagesFor(objectName="author", showDuplicates=false)#\n</cfoutput>\n\nPrevents duplicate messages from appearing multiple times in the list.\n\nExample 4 — Include or exclude associated objects\n<cfoutput>\n<!--- Only show errors on this object, not on associated objects --->\n#errorMessagesFor(objectName="author", includeAssociations=false)#\n</cfoutput>\n\nUseful if you want to display errors for the main object separately from associated objects (like a nested profile or address).\n\nExample 5 — HTML encoding disabled\n<cfoutput>\n#errorMessagesFor(objectName="author", encode=false)#\n</cfoutput>\n\nErrors are output as-is, allowing embedded HTML in the messages (use with caution).\n
"},"hint":"Builds and returns a list (ul tag with a default class of error-messages) containing all the error messages for all the properties of the object.\nReturns an empty string if no errors exist.\n\n","returntype":"string","slug":"controller.errorMessagesFor","parameters":[{"required":true,"hint":"The variable name of the object to display error messages for.","name":"objectName","type":"string"},{"default":"error-messages","required":false,"hint":"CSS `class` to set on the `ul` element.","name":"class","type":"string"},{"default":true,"required":false,"hint":"Whether or not to show duplicate error messages.","name":"showDuplicates","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"},{"default":true,"required":false,"name":"includeAssociations","type":"boolean"}],"availableIn":["controller"],"name":"errorMessagesFor","tags":{"categoryClass":"errorfunctions","sectionClass":"viewhelpers","category":"Error Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfscript>\nuser = model("user").findByKey(12);\n\nerrors = user.errorsOn("emailAddress");\n\nwriteDump(errors);\n</cfscript>\n\nReturns an array of error objects associated with the emailAddress property.\n\nEach element typically contains the error message and metadata like name or type.\n\nExample 2 — Filter by error name\n<cfscript>\nerrors = user.errorsOn("emailAddress", "uniqueEmail");\n\nwriteDump(errors);\n</cfscript>\n\nReturns only errors for emailAddress that have the error name uniqueEmail.\n\nExample 3 — Checking if a property has any errors\n<cfscript>\nif (arrayLen(user.errorsOn("password")) > 0) {\n writeOutput("Password has errors!");\n}\n</cfscript>\n\nThis is helpful when you need conditional logic based on whether a field has errors.\n\nExample 4 — Iterating over errors\n<cfscript>\nerrors = user.errorsOn("username");\n\nfor (var e in errors) {\n writeOutput("Error: " & e.message & "<br>");\n}\n</cfscript>\n\nLoops through all errors on a property and outputs the messages individually.\n
"},"hint":"errorsOn() returns an array of all errors associated with a specific property of a model object. You can also filter by a specific error name if needed. This is useful when you need programmatic access to errors rather than just displaying them in the view.\n\n","returntype":"array","slug":"model.errorsOn","parameters":[{"required":true,"hint":"Specify the property name to return errors for here.","name":"property","type":"string"},{"default":"","required":false,"hint":"If you want to return only errors on the property set with a specific error name you can specify it here.","name":"name","type":"string"}],"availableIn":["model"],"name":"errorsOn","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Get all base errors\n<cfscript>\nuser = model("user").findByKey(12);\n\nerrors = user.errorsOnBase();\n\nwriteDump(errors);\n</cfscript>\n\nReturns all general errors on the user object.\n\nEach element typically contains message, name, and type information.\n\nExample 2 — Filter by error name\n<cfscript>\nerrors = user.errorsOnBase("accountLocked");\n\nwriteDump(errors);\n</cfscript>\n\nReturns only base errors that have the error name accountLocked.\n\nExample 3 — Conditional logic with base errors\n<cfscript>\nif (arrayLen(user.errorsOnBase()) > 0) {\n writeOutput("There are general errors on this user account.");\n}\n</cfscript>\n\nThis can be used to block actions or display notices when object-level errors exist.\n\nExample 4 — Iterating over base errors\n<cfscript>\nfor (var e in user.errorsOnBase()) {\n writeOutput("General error: " & e.message & "<br>");\n}\n</cfscript>\n\nLoops through each object-level error and outputs its message.\n
"},"hint":"errorsOnBase() returns an array of all errors associated with the object as a whole, not tied to any specific property. This is useful for general errors such as system-level validations, cross-field validations, or custom errors added at the object level.\n\n","returntype":"array","slug":"model.errorsOnBase","parameters":[{"default":"","required":false,"hint":"Specify an error name here to only return errors for that error name.","name":"name","type":"string"}],"availableIn":["model"],"name":"errorsOnBase","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfscript>\ntext = "Wheels is a Rails-like MVC framework for Adobe ColdFusion and Lucee";\n\nsnippet = excerpt(text=text, phrase="framework", radius=5);\n\nwriteOutput(snippet);\n</cfscript>\n\nOutput:\n\n... MVC framework for ...\n\nExtracts 5 characters before and after "framework".\n\nAdds ... at the start and end to indicate truncation.\n\nExample 2 — Increase radius\n<cfscript>\nsnippet = excerpt(text=text, phrase="framework", radius=20);\n\nwriteOutput(snippet);\n</cfscript>\n\nOutput:\n\n... Rails-like MVC framework for Adobe Cold...\n\nShows more surrounding context (20 characters before and after the phrase).\n\nExample 3 — Custom excerpt string\n<cfscript>\nsnippet = excerpt(\n text=text,\n phrase="framework",\n radius=10,\n excerptString="***"\n);\n\nwriteOutput(snippet);\n</cfscript>\n\nOutput:\n\n*** Rails-like MVC framework for Adob ***\n\nUses *** instead of ... to mark truncated text.\n
"},"hint":"excerpt() extracts a portion of text surrounding the first instance of a given phrase. This is useful for previews, search result snippets, or highlighting context around a keyword.\n\n","returntype":"string","slug":"controller.excerpt","parameters":[{"required":true,"hint":"The text to extract an excerpt from.","name":"text","type":"string"},{"required":true,"hint":"The phrase to extract.","name":"phrase","type":"string"},{"default":100,"required":false,"hint":"Number of characters to extract surrounding the phrase.","name":"radius","type":"numeric"},{"default":"...","required":false,"hint":"String to replace first and / or last characters with.","name":"excerptString","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"excerpt","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\nfunction up() {\n  transaction {\n     // Execute a raw SQL statement\n     execute(sql="INSERT INTO users (firstname, lastname, email) VALUES ('John', 'Doe', 'john@example.com')");\n  }\n}\n</cfscript>\n
"},"hint":"execute() allows you to run a raw SQL query directly from a migration file. This is useful when you need to perform operations that aren’t easily handled by the built-in migration methods like createTable() or addColumn(). Only available in a migration CFC\n\n","returntype":"void","slug":"migration.execute","parameters":[{"required":true,"hint":"Arbitary SQL String","name":"sql","type":"string"}],"availableIn":["migration"],"name":"execute","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Checking if Joe exists in the database\nresult = model("user").exists(where="firstName = 'Joe'");\n\n2. Checking if a specific user exists based on a primary key valued passed in through the URL/form in an if statement\nif (model("user").exists(keyparams.key))\n{\n\t// Do something\n}\n\n3. If you have a `belongsTo` association setup from `comment` to `post`, you can do a scoped call. (The `hasPost` method below will call `model("post").exists(comment.postId)` internally.)\ncomment = model("comment").findByKey(params.commentId);\ncommentHasAPost = comment.hasPost();\n\n4. If you have a `hasOne` association setup from `user` to `profile`, you can do a scoped call. (The `hasProfile` method below will call `model("profile").exists(where="userId=#user.id#")` internally.)\nuser = model("user").findByKey(params.userId);\nuserHasProfile = user.hasProfile();\n\n5. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `hasComments` method below will call `model("comment").exists(where="postid=#post.id#")` internally.)\npost = model("post").findByKey(params.postId);\npostHasComments = post.hasComments();
"},"hint":"Checks if a record exists in the table.\nYou can pass in either a primary key value to the key argument or a string to the where argument.\nIf you don't pass in either of those, it will simply check if any record exists in the table.\n\n","returntype":"boolean","slug":"model.exists","parameters":[{"required":false,"hint":"Primary key value(s) of the record. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"exists","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Simple fail with no message\nfunction test_should_fail_on_purpose() {\n fail();\n};\n\nMarks the test as failed without explanation.\n\n2. Fail with a custom message\nfunction test_should_fail_with_a_message() {\n fail("This path should never be reached!");\n};\n\nProduces a failure with the message This path should never be reached!.\n\n3. Guarding unexpected conditions\nfunction test_should_not_allow_null_users() {\n var user = getUserById(123);\n if (isNull(user)) {\n     fail("Expected user with ID 123 to exist but got null.");\n }\n};\n
"},"hint":"Forces a test to fail intentionally. You can call fail() inside a test when you want to stop execution and explicitly mark the test as failed or highlight cases that should never happen. When called, it throws an exception that results in a test failure. You can optionally pass a custom message to clarify why the failure occurred. Used in wheels legacy testing.\n\n","returntype":"void","slug":"test.fail","parameters":[{"default":"","required":false,"name":"message","type":"string"}],"availableIn":["test"],"name":"fail","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Provide a `label` and the required `objectName` and `property`\n#fileField(label="Photo", objectName="photo", property="imageFile")#\n\n\n2. Display fields for photos provided by the `screenshots` association and nested properties\n<fieldset>\n\t<legend>Screenshots</legend>\n\t<cfloop from="1" to="#ArrayLen(site.screenshots)#" index="i">\n\t\t#fileField(label="File ##i#", objectName="site", association="screenshots", position=i, property="file")#\n\t\t#textField(label="Caption ##i#", objectName="site", association="screenshots", position=i, property="caption")#\n\t</cfloop>\n</fieldset>\n
"},"hint":"Builds and returns a string containing a file field form control based on the supplied objectName and property.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.fileField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"fileField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with label\n#fileFieldTag(label="Upload Photo", name="photo")#\n\nOutput:\n\n<label for="photo">Upload Photo</label>\n<input type="file" id="photo" name="photo">\n\n2. With custom attributes\n#fileFieldTag(\n label="Resume", \n name="resume", \n class="upload", \n id="resume-upload", \n accept=".pdf,.docx"\n)#\n\nAdds CSS class, ID, and file type restrictions.\n\n3. Label placement options\n#fileFieldTag(label="Avatar", name="avatar", labelPlacement="before")#\n#fileFieldTag(label="Attachment", name="attachment", labelPlacement="after")#\n\nMoves the label before or after the <input> instead of wrapping.\n\n4. Prepending/Appending markup\n#fileFieldTag(\n label="Select File", \n name="document", \n prepend='<div class="field-wrapper">', \n append='</div>'\n)#\n\nWraps the input inside a custom <div>.\n\n5. Label customization with prepend/append\n#fileFieldTag(\n label="Profile Photo", \n name="profile", \n prependToLabel='<span class="required">*</span>', \n appendToLabel=' <small>(max 2MB)</small>'\n)#\n
"},"hint":"Builds and returns a string containing a file form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.fileFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"fileFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Get filter chain.\nmyFilterChain = filterChain();\n\n2. Get filter chain for after filters only.\nmyFilterChain = filterChain(type="after");\n
"},"hint":"The filterChain() function returns an array of all filters that are set on the current controller in the order they will be executed. By default, it includes both before and after filters, but you can specify the type argument if you want to return only one type. For example, setting type=\"after\" will return only the filters that run after the controller action.\n\n","returntype":"array","slug":"controller.filterChain","parameters":[{"default":"all","required":false,"hint":"Use this argument to return only before or after filters.","name":"type","type":"string"}],"availableIn":["controller"],"name":"filterChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Run a filter before all actions\n// Always execute restrictAccess before every action\nfilters("restrictAccess");\n\n2. Multiple filters before all actions\n// Run both isLoggedIn and checkIPAddress before all actions\nfilters(through="isLoggedIn, checkIPAddress");\n\n3. Exclude specific actions\n// Run filters before all actions, except home and login\nfilters(through="isLoggedIn, checkIPAddress", except="home, login");\n\n4. Limit filters to specific actions\n// Only run ensureAdmin before the delete action\nfilters(through="ensureAdmin", only="delete");\n\n5. Run filters after an action\n// Run logAction after every action\nfilters(through="logAction", type="after");\n
"},"hint":"The filters() function lets you specify methods in your controller that should run automatically either before or after certain actions. Filters are useful for handling cross-cutting concerns such as authentication, authorization, logging, or cleanup, without having to repeat the same code inside each action. By default, filters run before the action, but you can configure them to run after, limit them to specific actions, exclude them from others, or control their placement in the filter chain.\n\n","returntype":"void","slug":"controller.filters","parameters":[{"required":true,"hint":"Function(s) to execute before or after the action(s).","name":"through","type":"string"},{"default":"before","required":false,"hint":"Whether to run the function(s) before or after the action(s).","name":"type","type":"string"},{"default":"","required":false,"hint":"Pass in a list of action names (or one action name) to tell Wheels that the filter function(s) should only be run on these actions.","name":"only","type":"string"},{"default":"","required":false,"hint":"Pass in a list of action names (or one action name) to tell Wheels that the filter function(s) should be run on all actions except the specified ones.","name":"except","type":"string"},{"default":"append","required":false,"hint":"Pass in `prepend` to prepend the function(s) to the filter chain instead of appending.","name":"placement","type":"string"}],"availableIn":["controller"],"name":"filters","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Getting only 5 users and ordering them randomly\nfiveRandomUsers = model("user").findAll(maxRows=5, order="random");\n\n2. Including an association (which in this case needs to be setup as a `belongsTo` association to `author` on the `article` model first)\narticles = model("article").findAll( include="author", where="published=1", order="createdAt DESC" );\n\n3. Similar to the above but using the association in the opposite direction (which needs to be setup as a `hasMany` association to `article` on the `author` model)\nbobsArticles = model("author").findAll( include="articles", where="firstName='Bob'" );\n\n4. Using pagination (getting records 26-50 in this case) and a more complex way to include associations (a song `belongsTo` an album, which in turn `belongsTo` an artist)\nsongs = model("song").findAll( include="album(artist)", page=2, perPage=25 );\n\n5. Using a dynamic finder to get all books released a certain year. Same as calling model("book").findOne(where="releaseYear=#params.year#")\nbooks = model("book").findAllByReleaseYear(params.year);\n\n6. Getting all books of a certain type from a specific year by using a dynamic finder. Same as calling  model("book").findAll( where="releaseYear=#params.year# AND type='#params.type#'" )\nbooks = model("book").findAllByReleaseYearAndType( "#params.year#,#params.type#" );\n\n7. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `comments` method below will call `model("comment").findAll(where="postId=#post.id#")` internally)\npost = model("post").findByKey(params.postId);\ncomments = post.comments();\n\n8. If you have an `Order` model with properties for `productId`, `amount` and a calculated property named `totalAmount` (set up as `property(name="totalAmount", sql="SUM(amount)")`), then you can do the following to get the ids for all products with over $1,000 in sales (the SQL will be created using `HAVING` instead of `WHERE` in this case since you're getting an aggregate value for a calculated property)\nids = model("order").findAll(group="productId", where="totalAmount > 1000");\n\n9. Using index hints\nindexes = {\n\tauthor="idx_authors_123",\n\tpost="idx_posts_123"\n}\nposts = model("author").findAll(where="firstname LIKE '#params.q#%' OR subject LIKE '#params.q#%'", include="posts", useIndex=indexes);\n
"},"hint":"Returns records from the database table mapped to this model according to the arguments passed in (use the where argument to decide which records to get, use the order argument to set the order in which those records should be returned, and so on).\nThe records will be returned as either a cfquery result set, an array of objects, or an array of structs (depending on what the returnAs argument is set to).\n\n","returntype":"any","slug":"model.findAll","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":"","required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"},{"default":"","required":false,"hint":"Determines how the `SELECT` clause for the query used to return data will look. You can pass in a list of the properties (which map to columns) that you want returned from your table(s). If you don't set this argument at all, Wheels will select all properties from your table(s). If you specify a table name (e.g. `users.email`) or alias a column (e.g. `fn AS firstName`) in the list, then the entire list will be passed through unchanged and used in the `SELECT` clause of the query. By default, all column names in tables joined via the `include` argument will be prepended with the singular version of the included table name.","name":"select","type":"string"},{"default":"false","required":false,"hint":"Whether to add the `DISTINCT` keyword to your `SELECT` clause. Wheels will, when necessary, add this automatically (when using pagination and a `hasMany` association is used in the `include` argument, to name one example).","name":"distinct","type":"boolean"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"-1","required":false,"hint":"Maximum number of records to retrieve. Passed on to the `maxRows` `cfquery` attribute. The default, `-1`, means that all records will be retrieved.","name":"maxRows","type":"numeric"},{"default":"0","required":false,"hint":"If you want to paginate records, you can do so by specifying a page number here. For example, getting records 11-20 would be page number 2 when `perPage` is kept at the default setting (10 records per page). The default, 0, means that records won't be paginated and that the `perPage` and `count` arguments will be ignored.","name":"page","type":"numeric"},{"default":10,"required":false,"hint":"When using pagination, you can specify how many records you want to fetch per page here. This argument is only used when the `page` argument has been passed in.","name":"perPage","type":"numeric"},{"default":"0","required":false,"hint":"When using pagination and you know in advance how many records you want to paginate through, you can pass in that value here. Doing so will prevent Wheels from running a `COUNT` query to get this value. This argument is only used when the `page` argument has been passed in.","name":"count","type":"numeric"},{"default":"query","required":false,"hint":"Handle to use for the query. This is used when you're paginating multiple queries and need to reference them individually in the `paginationLinks()` function. It's also used to set the name of the query in the debug output (which otherwise defaults to `userFindAllQuery` for example).","name":"handle","type":"string"},{"default":"","required":false,"hint":"If you want to cache the query, you can do so by specifying the number of minutes you want to cache the query for here. If you set it to `true`, the default cache time will be used (60 minutes).","name":"cache","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"query","required":false,"hint":"Set to `objects` to return an array of objects, set to `structs` to return a struct of structs, set to `array` to return an array of structs, set to `query` to return a query result set, or set to 'sql' to return the executed SQL query as a string.","name":"returnAs","type":"string"},{"default":true,"required":false,"hint":"When `returnAs` is set to `objects`, you can set this argument to `false` to prevent returning objects fetched from associations specified in the `include` argument. This is useful when you only need to include associations for use in the `WHERE` clause only and want to avoid the performance hit that comes with object creation.","name":"returnIncluded","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"[runtime expression]","required":false,"hint":"Override the default datasource","name":"dataSource","type":"string"},{"default":"0","required":false,"name":"$limit","type":"numeric"},{"default":"0","required":false,"name":"$offset","type":"numeric"}],"availableIn":["model"],"name":"findAll","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get all IDs for a model (basic usage):\n\nartistIds = model("artist").findAllKeys();\n\nReturns a comma-delimited list of all artist IDs.\n\n2. Get active artist IDs with custom delimiter and quotes:\n\nartistIds = model("artist").findAllKeys(quoted=true, delimiter="|", where="active=1");\n\nReturns only active artist IDs, quoted and separated with |.\n\n3. Limit results (top 10 user IDs):\n\nuserIds = model("user").findAllKeys(maxRows=10, order="createdAt DESC");\n\nReturns the 10 most recently created user IDs.\n\n4. Paginated IDs (books, second page):\n\nbookIds = model("book").findAllKeys(page=2, perPage=20, order="title ASC");\n\nFetches IDs for books on page 2 (records 21–40), ordered alphabetically.\n\n5. Grouped query with HAVING (order IDs by sales total):\n\norderIds = model("order").findAllKeys(group="productId", where="totalAmount > 500");\n\nReturns order IDs for products that generated more than $500 in sales.\n
"},"hint":"The findAllKeys() function retrieves all primary key values for a model’s records and returns them as a list. By default, the values are separated with commas, but you can change the delimiter with the delimiter argument or add single quotes around each value with the quoted argument. Since findAllKeys() accepts all arguments that findAll() does, you can also filter results with where, control ordering with order, or even include associations when filtering. This makes it useful when you need just the IDs of records without fetching full objects or rows.\n\n","returntype":"string","slug":"model.findAllKeys","parameters":[{"default":"false","required":false,"hint":"Set to `true` to enclose each value in single-quotation marks.","name":"quoted","type":"boolean"},{"default":",","required":false,"hint":"The delimiter character to separate the list items with.","name":"delimiter","type":"string"}],"availableIn":["model"],"name":"findAllKeys","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Getting the author with the primary key value `99` as an object\nauth = model("author").findByKey(99);\n\n2. Getting an author based on a form/URL value and then checking if it was found\nauth = model("author").findByKey(params.key);\nif(!isObject(auth)){\n    flashInsert(message="Author #params.key# was not found");\n    redirectTo(back=true);\n}\n\n3. If you have a `belongsTo` association setup from `comment` to `post`, you can do a scoped call. (The `post` method below will call `model("post").findByKey(comment.postId)` internally)\ncomment = model("comment").findByKey(params.commentId);\npost = comment.post();
"},"hint":"The findByKey() function retrieves a single record from the database using its primary key value and returns it as an object by default. If the record is not found, it returns false, making it easy to handle missing data gracefully. You can also control what columns are returned using the select argument, include related associations, or override the return format to a query, struct, or even raw SQL. Since it accepts the same options as other read functions like findOne(), you can apply caching, indexing, and even include soft-deleted records when needed.\n\n","returntype":"any","slug":"model.findByKey","parameters":[{"required":true,"hint":"Primary key value(s) of the record. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"default":"","required":false,"hint":"Determines how the `SELECT` clause for the query used to return data will look. You can pass in a list of the properties (which map to columns) that you want returned from your table(s). If you don't set this argument at all, Wheels will select all properties from your table(s). If you specify a table name (e.g. `users.email`) or alias a column (e.g. `fn AS firstName`) in the list, then the entire list will be passed through unchanged and used in the `SELECT` clause of the query. By default, all column names in tables joined via the `include` argument will be prepended with the singular version of the included table name.","name":"select","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"query","required":false,"hint":"Handle to use for the query. This is used to set the name of the query in the debug output (which otherwise defaults to `userFindOneQuery` for example).","name":"handle","type":"string"},{"default":"","required":false,"hint":"If you want to cache the query, you can do so by specifying the number of minutes you want to cache the query for here. If you set it to `true`, the default cache time will be used (60 minutes).","name":"cache","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"object","required":false,"hint":"Set to `objects` to return an array of objects, set to `structs` to return a struct of structs, set to `array` to return an array of structs, set to `query` to return a query result set, or set to 'sql' to return the executed SQL query as a string.","name":"returnAs","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Override the default datasource","name":"dataSource","type":"string"}],"availableIn":["model"],"name":"findByKey","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get the first record by primary key (default behavior):\n\nfirstUser = model("user").findFirst();\n\nFetches the user with the lowest primary key value.\n\n2. Get the first record alphabetically by name:\n\nfirstAuthor = model("author").findFirst(property="lastName");\n\nFetches the author with the alphabetically first last name.\n\n3. Get the earliest created record (using a timestamp column):\n\nfirstArticle = model("article").findFirst(property="createdAt");\n\nFetches the oldest article based on creation date.\n\n4. Get the cheapest product:\n\ncheapestProduct = model("product").findFirst(property="price");\n\nFetches the product with the lowest price.\n\n5. Use alias properties instead of property:\n\nfirstComment = model("comment").findFirst(properties="createdAt");\n\nWorks the same as property — useful when you prefer the plural alias.\n
"},"hint":"The findFirst() function fetches the first record from the database table mapped to the model, ordered by the primary key value by default. You can customize the ordering by passing a property name through the property argument, which is also aliased as properties. This makes it useful when you want the \"first\" record based on a specific field (e.g., earliest created date, alphabetically first name, lowest price, etc.). The result is returned as a model object.\n\n","returntype":"any","slug":"model.findFirst","parameters":[{"default":"[runtime expression]","required":false,"hint":"Name of the property to order by. This argument is also aliased as `properties`.","name":"property","type":"string"},{"default":"ASC","required":false,"name":"$sort","type":"string"}],"availableIn":["model"],"name":"findFirst","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get the last record by primary key (default behavior):\n\nlastUser = model("user").findLastOne();\n\nFetches the user with the highest primary key value.\n\n2. Get the last record alphabetically by name:\n\nlastAuthor = model("author").findLastOne(property="lastName");\n\nFetches the author with the alphabetically last last name.\n\n3. Get the most recently created record:\n\nlastArticle = model("article").findLastOne(property="createdAt");\n\nFetches the article with the latest creation date.\n\n4. Get the most expensive product:\n\npriciestProduct = model("product").findLastOne(property="price");\n\nFetches the product with the highest price.\n\n5. Use alias properties instead of property:\n\nlastComment = model("comment").findLastOne(properties="createdAt");\n\nWorks the same as property — useful when you prefer the plural alias.\n
"},"hint":"The findLastOne() function fetches the last record from the database table mapped to the model, ordered by the primary key value by default. You can override this ordering by passing a property name through the property argument (also aliased as properties). This is useful when you want to retrieve the \"last\" record based on something other than the primary key, such as the most recently created entry, the highest price, or the latest updated timestamp. The result is returned as a model object. This function was formerly known as findLast.\n\n","returntype":"any","slug":"model.findLastOne","parameters":[{"required":false,"hint":"Name of the property to order by. This argument is also aliased as `properties`.","name":"property","type":"string"}],"availableIn":["model"],"name":"findLastOne","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Getting the most recent order as an object from the database\norder = model("order").findOne(order="datePurchased DESC");\n\n2. Using a dynamic finder to get the first person with the last name `Smith`. Same as calling `model("user").findOne(where"lastName='Smith'")`\nperson = model("user").findOneByLastName("Smith");\n\n3. Getting a specific user using a dynamic finder. Same as calling `model("user").findOne(where"email='someone@somewhere.com' AND password='mypass'")`\nuser = model("user").findOneByEmailAndPassword("someone@somewhere.com,mypass");\n\n4. If you have a `hasOne` association setup from `user` to `profile`, you can do a scoped call. (The `profile` method below will call `model("profile").findOne(where="userId=#user.id#")` internally)\nuser = model("user").findByKey(params.userId);\nprofile = user.profile();\n\n5. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `findOneComment` method below will call `model("comment").findOne(where="postId=#post.id#")` internally)\npost = model("post").findByKey(params.postId);\ncomment = post.findOneComment(where="text='I Love Wheels!'");
"},"hint":"Fetches the first record found based on the WHERE and ORDER BY clauses.\nWith the default settings (i.e. the returnAs argument set to object), a model object will be returned if the record is found and the boolean value false if not.\nInstead of using the where argument, you can create cleaner code by making use of a concept called Dynamic Finders.\n\n","returntype":"any","slug":"model.findOne","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":"","required":false,"hint":"Determines how the `SELECT` clause for the query used to return data will look. You can pass in a list of the properties (which map to columns) that you want returned from your table(s). If you don't set this argument at all, Wheels will select all properties from your table(s). If you specify a table name (e.g. `users.email`) or alias a column (e.g. `fn AS firstName`) in the list, then the entire list will be passed through unchanged and used in the `SELECT` clause of the query. By default, all column names in tables joined via the `include` argument will be prepended with the singular version of the included table name.","name":"select","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"query","required":false,"hint":"Handle to use for the query. This is used to set the name of the query in the debug output (which otherwise defaults to `userFindOneQuery` for example).","name":"handle","type":"string"},{"default":"","required":false,"hint":"If you want to cache the query, you can do so by specifying the number of minutes you want to cache the query for here. If you set it to `true`, the default cache time will be used (60 minutes).","name":"cache","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"object","required":false,"hint":"Set to `objects` to return an array of objects, set to `structs` to return a struct of structs, set to `array` to return an array of structs, set to `query` to return a query result set, or set to 'sql' to return the executed SQL query as a string.","name":"returnAs","type":"string"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"[runtime expression]","required":false,"hint":"Override the default datasource","name":"dataSource","type":"string"}],"availableIn":["model"],"name":"findOne","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get a specific Flash value (commonly used for notifications or messages)\n\nnotice = flash("notice");\n\n2. Get another value stored in Flash, e.g., an error message\n\nerrorMsg = flash("error");\n\n3. Retrieve the entire Flash scope as a struct\n\nallFlash = flash();\n
"},"hint":"The flash() function is used in controllers to access data stored in the Flash scope. Flash is a temporary storage mechanism that lets you persist values across the next request (often after a redirect). You can use it to retrieve a specific key or the entire Flash struct. If you pass in a key, it returns the value associated with it; if no key is passed, it returns all the Flash contents as a struct.\n\n","returntype":"any","slug":"controller.flash","parameters":[{"required":false,"hint":"The key to get the value for.","name":"key","type":"string"}],"availableIn":["controller"],"name":"flash","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Clear all flash values at the start of an action\nflashClear();\n\n2. Clear messages after they've been displayed\nnotice = flash("notice");\nif (len(notice)) {\n    writeOutput(notice);\n    flashClear(); // reset Flash so it doesn't show again\n}\n\n3. Use before redirecting if you want to ensure no old flash values remain\nflashClear();\nredirectTo(action="index");\n
"},"hint":"The flashClear() function removes all keys and values from the Flash scope. This is useful when you want to reset or clear out any temporary messages or data that were carried over from a previous request. After calling flashClear(), the Flash will be empty for the remainder of the request and any future requests until new values are inserted.\n\n","returntype":"void","slug":"controller.flashClear","parameters":[],"availableIn":["controller"],"name":"flashClear","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get the number of items in Flash\ncount = flashCount();\n\n2. Check if there are any flash messages before displaying\nif (flashCount() > 0) {\n    writeOutput("You have " & flashCount() & " messages in Flash.");\n}\n\n3. Only display notice if Flash is not empty\nif (flashCount() > 0 && structKeyExists(flash(), "notice")) {\n    writeOutput(flash("notice"));\n}\n
"},"hint":"The flashCount() function returns the number of keys currently stored in the Flash scope. This is useful to check whether there are any flash messages or temporary data before attempting to read or display them. It helps in conditionally rendering notifications or determining if the Flash is empty.\n\n","returntype":"numeric","slug":"controller.flashCount","parameters":[],"availableIn":["controller"],"name":"flashCount","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Delete a single flash message\nflashDelete(key="errorMessage");\n\n2. Delete another key and check if it existed\nif (flashDelete(key="notice")) {\n    writeOutput("Notice deleted from Flash.");\n} else {\n    writeOutput("Notice key did not exist.");\n}\n\n3. Conditional usage before displaying flash\nif (structKeyExists(flash(), "warning")) {\n    warningMsg = flash("warning");\n    flashDelete(key="warning"); // remove after reading\n    writeOutput(warningMsg);\n}\n
"},"hint":"The flashDelete() function removes a specific key from the Flash scope. It is useful when you want to delete a particular temporary message or piece of data without clearing the entire Flash. The function returns true if the key existed and was deleted, or false if the key was not present.\n\n","returntype":"any","slug":"controller.flashDelete","parameters":[{"required":true,"hint":"The key to delete","name":"key","type":"string"}],"availableIn":["controller"],"name":"flashDelete","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Insert a simple flash message\nflashInsert(msg="It Worked!");\n\n2. Insert multiple types of data\nflashInsert(userId=123);\nflashInsert(errorMessage="Something went wrong");\n\n3. Typical usage: insert a message before redirecting\nflashInsert(notice="Profile updated successfully");\nredirectTo(action="show");\n\n4. Insert a structured value\nflashInsert(userStruct={id=42, name="Alice"});\n
"},"hint":"The flashInsert() function adds a new key-value pair to the Flash scope. This is useful for storing temporary messages or data that you want to persist across the next request, typically after a redirect. You can insert any type of value, such as strings, numbers, or structs, and later retrieve it using flash().\n\n","returntype":"void","slug":"controller.flashInsert","parameters":[],"availableIn":["controller"],"name":"flashInsert","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Check if the Flash is empty\nif (flashIsEmpty()) {\n    writeOutput("No messages to display.");\n} else {\n    writeOutput("There are messages in Flash.");\n}\n\n2. Use before reading a specific key\nif (!flashIsEmpty() && structKeyExists(flash(), "notice")) {\n    writeOutput(flash("notice"));\n}\n\n3. Typical flow: after clearing Flash\nflashClear();\nwriteOutput(flashIsEmpty()); // true\n
"},"hint":"The flashIsEmpty() function checks whether the Flash scope contains any keys. It returns true if the Flash is empty and false if it contains one or more keys. This is useful for conditionally displaying messages or deciding whether to process Flash data before reading or clearing it.\n\n","returntype":"boolean","slug":"controller.flashIsEmpty","parameters":[],"availableIn":["controller"],"name":"flashIsEmpty","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Keep the entire Flash for the next request\nflashKeep();\n\n2. Keep the "error" key in the Flash for the next request\nflashKeep("error");\n\n3. Keep both the "error" and "success" keys in the Flash for the next request\nflashKeep("error,success");
"},"hint":"The flashKeep() function allows you to preserve Flash data for one additional request. By default, Flash values are only available for the very next request; calling flashKeep() prevents them from being cleared after the current request. You can choose to keep the entire Flash or only specific keys. This is useful when you want messages or temporary data to persist through multiple redirects or page loads.\n\n","returntype":"void","slug":"controller.flashKeep","parameters":[{"default":"","required":false,"name":"key","type":"string"}],"availableIn":["controller"],"name":"flashKeep","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Check if the "error" key exists\nerrorExists = flashKeyExists("error");\n\n2. Conditional display based on key existence\nif (flashKeyExists("notice")) {\n    writeOutput(flash("notice"));\n}\n\n3. Example usage in a form flow\nif (flashKeyExists("validationErrors")) {\n    errors = flash("validationErrors");\n    // Process or display errors\n}\n
"},"hint":"The flashKeyExists() function checks whether a specific key is present in the Flash scope. It returns true if the key exists and false if it does not. This is useful for conditionally displaying or processing Flash messages or data before attempting to read them.\n\n","returntype":"boolean","slug":"controller.flashKeyExists","parameters":[{"required":true,"hint":"The key to check.","name":"key","type":"string"}],"availableIn":["controller"],"name":"flashKeyExists","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
// In the controller action\nflashInsert(success="Your post was successfully submitted.");\nflashInsert(alert="Don't forget to tweet about this post!");\nflashInsert(error="This is an error message.");\n\n<!--- In the layout or view --->\n#flashMessages()#\n\n<!--- Generates this (sorted alphabetically):--->\n<div class="flashMessages">\n\t<p class="alertMessage">\n\t\tDon't forget to tweet about this post!\n\t</p>\n\t<p class="errorMessage">\n\t\tThis is an error message.\n\t</p>\n\t<p class="successMessage">\n\t\tYour post was successfully submitted.\n\t</p>\n</div>\n\n\n<!---  Only show the "success" key in the view --->\n#flashMessages(key="success")#\n\n<!--- Generates this: --->\n<div class="flashMessage">\n\t<p class="successMessage">\n\t\tYour post was successfully submitted.\n\t</p>\n</div>\n\n\n<!--- Show only the "success" and "alert" keys in the view, in that order --->\n#flashMessages(keys="success,alert")#\n\n<!--- Generates this (sorted alphabetically):--->\n<div class="flashMessages">\n\t<p class="successMessage">\n\t\tYour post was successfully submitted.\n\t</p>\n\t<p class="alertMessage">\n\t\tDon't forget to tweet about this post!\n\t</p>\n</div>
"},"hint":"The flashMessages() function generates a formatted HTML output of messages stored in the Flash scope. It is typically used in views or layouts to display temporary notifications like success messages, alerts, or errors. You can choose to display all messages, a specific key, or multiple keys in a defined order. Additional options let you customize the container’s HTML class, include an empty container if no messages exist, and control whether the message content is URL-encoded.\n\n","returntype":"string","slug":"controller.flashMessages","parameters":[{"required":false,"hint":"The key (or list of keys) to show the value for. You can also use the `key` argument instead for better readability when accessing a single key.","name":"keys","type":"string"},{"default":"flash-messages","required":false,"hint":"HTML `class` to set on the `div` element that contains the messages.","name":"class","type":"string"},{"default":"false","required":false,"hint":"Includes the `div` container even if the Flash is empty.","name":"includeEmptyContainer","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"flashMessages","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: add a single float column\nt.float("price");\n\n2. Add multiple float columns at once\nt.float("length,width,height");\n\n3. Add a float column with a default value\nt.float(columnNames="discount", default="0.0");\n\n4. Add a float column that cannot be null\nt.float(columnNames="taxRate", allowNull=false);\n\n5. Add multiple float columns with defaults\nt.float(columnNames="latitude,longitude", default="0.0");\n\n6. Combine default value and null constraint\nt.float(columnNames="weight", default="1.0", allowNull=false);\n
"},"hint":"The float() function is used in a table definition during a migration to add one or more float-type columns to a database table. You can specify column names, default values, and whether the columns allow NULL. This helps define numeric columns with decimal values in your schema. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.float","parameters":[{"required":false,"name":"columnNames","type":"string"},{"default":"","required":false,"name":"default","type":"string"},{"default":"true","required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"float","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  post\n    // Example URL: /posts/my-post-title\n    // Controller:  Posts\n    // Action:      show\n    .get(name="post", pattern="posts/[slug]", to="posts##show")\n\n    // Route name:  posts\n    // Example URL: /posts\n    // Controller:  Posts\n    // Action:      index\n    .get(name="posts", controller="posts", action="index")\n\n    // Route name:  authors\n    // Example URL: /the-scribes\n    // Controller:  Authors\n    // Action:      index\n    .get(name="authors", pattern="the-scribes", to="authors##index")\n\n    // Route name:  commerceCart\n    // Example URL: /cart\n    // Controller:  commerce.Carts\n    // Action:      show\n    .get(name="cart", to="carts##show", package="commerce")\n\n    // Route name:  extranetEditProfile\n    // Example URL: /profile/edit\n    // Controller:  extranet.Profiles\n    // Action:      edit\n    .get(\n        name="editProfile",\n        pattern="profile/edit",\n        to="profiles##edit",\n        package="extranet"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="users", nested=true)\n        // Route name:  activatedUsers\n        // Example URL: /users/activated\n        // Controller:  Users\n        // Action:      activated\n        .get(name="activated", to="users##activated", on="collection")\n\n        // Route name:  preferencesUsers\n        // Example URL: /users/391/preferences\n        // Controller:  Preferences\n        // Action:      index\n        .get(name="preferences", to="preferences##index", on="member")\n    .end()\n.end();\n\n</cfscript>\n
"},"hint":"Create a route that matches a URL requiring an HTTP GET method. We recommend only using this matcher to expose actions that display data. See post, patch, delete, and put for matchers that are appropriate for actions that change data in your database.\n\n","returntype":"struct","slug":"mapper.get","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"get","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Get the current value of a global Wheels setting\ntablePrefix = get("tableNamePrefix");\n\n2. Get the default message for the `validatesConfirmationOf` function\nconfirmationMessageDefault = get(functionName="validatesConfirmationOf", name="message");\n\n3. Check the default value for the "null" argument in migrations\nallowNullDefault = get(functionName="float", name="null");\n\n4. Retrieve the current default number of rows per page in pagination\nperPageDefault = get("perPage");\n
"},"hint":"Returns the current value of a Wheels configuration setting or the default value for a specific function argument. It can be used to inspect global Wheels settings (like table name prefixes, pagination defaults, or other configuration values) or to check what the default argument would be for a particular Wheels function.\n\n","returntype":"any","slug":"controller.get","parameters":[{"required":true,"hint":"Variable name to get setting for.","name":"name","type":"string"},{"default":"","required":false,"hint":"Function name to get setting for.","name":"functionName","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"get","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"configuration","category":"Miscellaneous Functions","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Get all available migrations in the default folder\nmigrations = application.wheels.migrator.getAvailableMigrations();\n\n// Determine the latest migration version\nif (ArrayLen(migrations)) {\n    latestVersion = migrations[ArrayLen(migrations)].version;\n} else {\n    latestVersion = 0;\n}\n\n2. Get available migrations from a custom folder\ncustomMigrations = application.wheels.migrator.getAvailableMigrations(path="/custom/migrations");\n\n// Loop through migrations and display their versions\nfor (var m in migrations) {\n    writeOutput("Migration version: " & m.version & "<br>");\n}\n
"},"hint":"The getAvailableMigrations() function scans the migration folder (by default /app/migrator/migrations/) and returns an array of all migration files it finds. Each item in the array contains information about the migration, including its version. While this function can be called from within your application, it is primarily intended for use via the Wheels CLI or GUI tools. It is useful for programmatically determining which migrations are available and what the latest migration version is.\n\n","returntype":"array","slug":"migrator.getAvailableMigrations","parameters":[{"default":"[runtime expression]","required":false,"hint":"Path to Migration Files: defaults to /migrator/migrations/","name":"path","type":"string"}],"availableIn":["migrator"],"name":"getAvailableMigrations","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get the current database migration version\ncurrentVersion = application.wheels.migrator.getCurrentMigrationVersion();\nwriteOutput("Current DB version: " & currentVersion);\n\n// Compare with the latest available migration version\nmigrations = application.wheels.migrator.getAvailableMigrations();\nif (ArrayLen(migrations)) {\n    latestVersion = migrations[ArrayLen(migrations)].version;\n    if (currentVersion LT latestVersion) {\n        writeOutput("Database is behind the latest migration.");\n    } else {\n        writeOutput("Database is up-to-date.");\n    }\n}\n\n// Conditional logic based on migration version\nif (currentVersion EQ "2023091501") {\n    // perform tasks specific to this version\n}\n
"},"hint":"The getCurrentMigrationVersion() function returns the version number of the latest migration that has been applied to the database. This is useful for determining the current schema state programmatically, though it is primarily intended for use via the Wheels CLI or GUI interface. You can use this function within your application to perform conditional logic based on the database version or to verify that the database is up-to-date.\n\n","returntype":"string","slug":"migrator.getCurrentMigrationVersion","parameters":[],"availableIn":["migrator"],"name":"getCurrentMigrationVersion","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get all emails sent during the current request\nemails = getEmails();\n\n// Check if an email was sent to a specific recipient\nfor (var email in emails) {\n    if (email.to EQ "user@example.com") {\n        writeOutput("Email sent to user@example.com<br>");\n    }\n}\n
"},"hint":"Primarily used in testing scenarios to retrieve information about the emails that were sent during the current request. It returns an array containing details of all sent emails, which allows you to verify the content, recipients, and other properties of the emails in your automated tests. This is especially useful for unit or functional tests where you want to assert that specific emails are being triggered by certain actions without actually sending them.\n\n","returntype":"array","slug":"controller.getEmails","parameters":[],"availableIn":["controller"],"name":"getEmails","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get all files sent during the current request\nfiles = getFiles();\n\n// Check if a specific file was sent\nfor (var file in files) {\n    if (file.name EQ "report.pdf") {\n        writeOutput("File 'report.pdf' was sent.<br>");\n    }\n}\n
"},"hint":"The getFiles() function is primarily used in testing scenarios to retrieve information about files sent during the current request. It returns an array containing details of all files handled in the request, such as uploaded attachments or generated files. This allows you to inspect and verify file-related operations in automated tests without needing to access the file system directly.\n\n","returntype":"array","slug":"controller.getFiles","parameters":[],"availableIn":["controller"],"name":"getFiles","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get redirect information for the current request\nredirectInfo = getRedirect();\n\n// Check if a redirect occurred\nif (structKeyExists(redirectInfo, "url")) {\n    writeOutput("Redirected to: " & redirectInfo.url);\n    writeOutput("HTTP status: " & redirectInfo.status);\n} else {\n    writeOutput("No redirect occurred.");\n}\n
"},"hint":"Primarily used in testing scenarios to determine whether the current request has performed a redirect. It returns a structure containing information about the redirect, such as the target URL and the HTTP status code. This allows you to verify redirect behavior in automated tests without actually sending the user to another page.\n\n","returntype":"struct","slug":"controller.getRedirect","parameters":[],"availableIn":["controller"],"name":"getRedirect","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get all defined routes\nallRoutes = application.wheels.mapper.getRoutes();\n\n// Loop through routes and display their patterns\nfor (var r in allRoutes) {\n    writeOutput("Route name: " & r.name & "<br>");\n    writeOutput("Pattern: " & r.pattern & "<br>");\n    writeOutput("Controller: " & r.controller & "<br>");\n    writeOutput("Action: " & r.action & "<br><br>");\n}\n
"},"hint":"Returns all the routes that have been defined in the application via the mapper() function. It provides a programmatic way to inspect the routing table, including route names, URL patterns, controllers, actions, and other metadata. This is useful for debugging, generating dynamic links, or performing logic based on the routes that exist in your application.","returntype":"any","slug":"mapper.getRoutes","parameters":[],"availableIn":["mapper"],"name":"getRoutes","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Get the table name prefix for the current model\nprefix = model(\"user\").getTableNamePrefix();\nwriteOutput(\"Table prefix: \" & prefix);\n\n2. Get the table name prefix for this user when running a custom query.\n<cffunction name="getDisabledUsers" returntype="query">\n\t<cfquery datasource="#get('dataSourceName')#" name="local.disabledUsers">\n\tSELECT *\n\tFROM #this.getTableNamePrefix()#users\n\tWHERE disabled = 1\n\t</cfquery>\n\t<cfreturn local.disabledUsers>\n</cffunction>\n
"},"hint":"Returns the table name prefix that is set for the current model. This is useful when your database tables share a common prefix, and you need to construct queries dynamically or perform operations that require the full table name. By using this function, you ensure consistency and avoid hardcoding table prefixes in your queries.\n\n","returntype":"string","slug":"model.getTableNamePrefix","parameters":[],"availableIn":["model"],"name":"getTableNamePrefix","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get a member object and change the `email` property on it\nmember = model("member").findByKey(params.memberId);\nmember.email = params.newEmail;\n\n2. Check if the `email` property has changed\nif(member.hasChanged("email")){\n    // Do something...\n}\n\n3. The above can also be done using a dynamic function like this\nif(member.emailHasChanged()){\n    // Do something...\n}
"},"hint":"Returns true if the specified property (or any if none was passed in) has been changed but not yet saved to the database.\nWill also return true if the object is new and no record for it exists in the database.\n\n","returntype":"boolean","slug":"model.hasChanged","parameters":[{"default":"","required":false,"hint":"Name of property to check for change.","name":"property","type":"string"}],"availableIn":["model"],"name":"hasChanged","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Get a post object\npost = model("post").findByKey(params.postId);\n\n// Check if the object has any errors\nif (post.hasErrors()) {\n    writeOutput("There are errors. Redirecting to the edit form...");\n    redirectTo(action="edit", id=post.id);\n}\n\n2. Check if a specific property has errors\nif (post.hasErrors(property="title")) {\n    writeOutput("The title field contains errors.");\n}\n\n3. Check if a specific named error exists\nif (post.hasErrors(name="requiredTitle")) {\n    writeOutput("The post is missing a required title.");\n}\n
"},"hint":"Checks whether a model object has any validation or other errors. It returns true if the object contains errors, or if a specific property or named error is provided, it checks only that subset. This is useful for validating objects before saving them to the database or displaying error messages to the user.\n\n","returntype":"boolean","slug":"model.hasErrors","parameters":[{"default":"","required":false,"hint":"Name of the property to check if there are any errors set on.","name":"property","type":"string"},{"default":"","required":false,"hint":"Error name to check if there are any errors set with.","name":"name","type":"string"}],"availableIn":["model"],"name":"hasErrors","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Specify that instances of this model has many comments (the table for the associated model, not the current, should have the foreign key set on it).\nhasMany("comments");\n\n2. Specify that this model (let's call it `reader` in this case) has many subscriptions and setup a shortcut to the `publication` model (useful when dealing with many-to-many relationships).\nhasMany(name="subscriptions", shortcut="publications");\n\n3. Automatically delete all associated `comments` whenever this object is deleted.\nhasMany(name="comments", dependent="deleteAll");\n\n// When not following Wheels naming conventions for associations, it can get complex to define how a `shortcut` works.\n// In this example, we are naming our `shortcut` differently than the actual model's name.\n\n4. In the app/models/Customer.cfc `config()` method.\nhasMany(name="subscriptions", shortcut="magazines", through="publication,subscriptions");\n\n// In the app/models/Subscription.cfc `config()` method.\nbelongsTo("customer");\nbelongsTo("publication");\n\n// In the app/models/Publication `config()` method.\nhasMany("subscriptions");\n
"},"hint":"Sets up a one-to-many association between the current model and another model. This allows you to easily fetch, join, and manage related records in a relational way while following Wheels conventions.\n\n","returntype":"void","slug":"model.hasMany","parameters":[{"required":true,"hint":"Gives the association a name that you refer to when working with the association (in the `include` argument to `findAll`, to name one example).","name":"name","type":"string"},{"default":"","required":false,"hint":"Name of associated model (usually not needed if you follow Wheels conventions because the model name will be deduced from the `name` argument).","name":"modelName","type":"string"},{"default":"","required":false,"hint":"Foreign key property name (usually not needed if you follow Wheels conventions since the foreign key name will be deduced from the `name` argument).","name":"foreignKey","type":"string"},{"default":"","required":false,"hint":"Column name to join to if not the primary key (usually not needed if you follow Wheels conventions since the join key will be the table's primary key/keys).","name":"joinKey","type":"string"},{"default":"outer","required":false,"hint":"Use to set the join type when joining associated tables. Possible values are `inner` (for `INNER JOIN`) and `outer` (for `LEFT OUTER JOIN`).","name":"joinType","type":"string"},{"default":false,"required":false,"hint":"Defines how to handle dependent model objects when you delete an object from this model. `delete` / `deleteAll` deletes the record(s) (`deleteAll` bypasses object instantiation). `remove` / `removeAll` sets the forein key field(s) to `NULL` (`removeAll` bypasses object instantiation).","name":"dependent","type":"string"},{"default":"","required":false,"hint":"Set this argument to create an additional dynamic method that gets the object(s) from the other side of a many-to-many association.","name":"shortcut","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Set this argument if you need to override Wheels conventions when using the `shortcut` argument. Accepts a list of two association names representing the chain from the opposite side of the many-to-many relationship to this model.","name":"through","type":"string"}],"availableIn":["model"],"name":"hasMany","tags":{"categoryClass":"associationfunctions","sectionClass":"modelconfiguration","category":"Association Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage (Books -> Authors)\n\n// Loop through all authors and render checkboxes for associating them with the current book\n<cfloop query="authors">\n    #hasManyCheckBox(\n        objectName="book",\n        association="bookAuthors",\n        keys="#book.key()#,#authors.id#",\n        label=authors.fullName\n    )#\n</cfloop>\n\n2. Custom label placement\n\n// Place the label after the checkbox instead of before\n<cfloop query="categories">\n    #hasManyCheckBox(\n        objectName="post",\n        association="postCategories",\n        keys="#post.key()#,#categories.id#",\n        label=categories.name,\n        labelPlacement="after"\n    )#\n</cfloop>\n\n3. Wrapping checkboxes in extra HTML (prepend/append)\n\n// Use prepend and append to wrap checkboxes in a <div> with a custom class\n<cfloop query="tags">\n    #hasManyCheckBox(\n        objectName="article",\n        association="articleTags",\n        keys="#article.key()#,#tags.id#",\n        label=tags.name,\n        prepend='<div class="tag-option">',\n        append='</div>'\n    )#\n</cfloop>\n
"},"hint":"The hasManyCheckBox() helper generates the correct form elements for managing a hasMany or many-to-many association. It creates checkboxes for linking records together (e.g., a Book with many Authors).\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hasManyCheckBox","parameters":[{"required":true,"hint":"Name of the variable containing the parent object to represent with this form field.","name":"objectName","type":"string"},{"required":true,"hint":"Name of the association set in the parent object to represent with this form field.","name":"association","type":"string"},{"required":true,"hint":"Primary keys associated with this form field. Note that these keys should be listed in the order that they appear in the database table.","name":"keys","type":"string"},{"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using `aroundLeft` or `aroundRight`.","name":"labelPlacement","type":"string"},{"required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"required":false,"hint":"The `class` name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"hasManyCheckBox","tags":{"categoryClass":"formassociationfunctions","sectionClass":"viewhelpers","category":"Form Association Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage (Author -> Default Address)\n\n// Pick one address as the author’s default\n<cfloop query="addresses">\n    #hasManyRadioButton(\n        objectName="author",\n        association="authorsDefaultAddresses",\n        property="defaultAddressId",\n        keys="#author.key()#,#addresses.id#",\n        tagValue="#addresses.id#",\n        label=addresses.title\n    )#\n</cfloop>\n\n2. Pre-check default radio if property is blank\n\n// If no address is selected yet, pre-check the "Home" option\n<cfloop query="addresses">\n    #hasManyRadioButton(\n        objectName="author",\n        association="authorsDefaultAddresses",\n        property="defaultAddressId",\n        keys="#author.key()#,#addresses.id#",\n        tagValue="#addresses.id#",\n        label=addresses.title,\n        checkIfBlank=(addresses.title EQ "Home")\n    )#\n</cfloop>\n\n3. Style with extra HTML attributes\n\n// Add class and id for custom styling\n<cfloop query="paymentMethods">\n    #hasManyRadioButton(\n        objectName="user",\n        association="userPaymentMethods",\n        property="defaultPaymentMethodId",\n        keys="#user.key()#,#paymentMethods.id#",\n        tagValue="#paymentMethods.id#",\n        label=paymentMethods.name,\n        class="radio-option",\n        id="paymentMethod_#paymentMethods.id#"\n    )#\n</cfloop>\n
"},"hint":"This helper generates radio buttons for managing a hasMany or one-to-many association, where you want the user to pick one option (e.g., default address, primary contact method, preferred category).\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hasManyRadioButton","parameters":[{"required":true,"hint":"Name of the variable containing the parent object to represent with this form field.","name":"objectName","type":"string"},{"required":true,"hint":"Name of the association set in the parent object to represent with this form field.","name":"association","type":"string"},{"required":true,"hint":"Name of the property in the child object to represent with this form field.","name":"property","type":"string"},{"required":true,"hint":"Primary keys associated with this form field. Note that these keys should be listed in the order that they appear in the database table.","name":"keys","type":"string"},{"required":true,"hint":"The value of the radio button when selected.","name":"tagValue","type":"string"},{"default":false,"required":false,"hint":"Whether or not to check this form field as a default if there is a blank value set for the property.","name":"checkIfBlank","type":"boolean"},{"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"hasManyRadioButton","tags":{"categoryClass":"formassociationfunctions","sectionClass":"viewhelpers","category":"Form Association Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic one-to-one association\n\n// A User has one Profile. The profiles table has userId as the foreign key.\n// In app/models/User.cfc\nhasOne("profile");\n\n2. Strict inner join\n\n// Force that every Employee must have one PayrollRecord.\n// In app/models/Employee.cfc\nhasOne(name="payrollRecord", joinType="inner");\n\n// If there is no matching payrollRecord, the employee will not appear in queries using this association.\n\n3. Auto-delete dependent record\n\n// Delete the Profile when the User is deleted.\n// In app/models/User.cfc\nhasOne(name="profile", dependent="delete");\n\n4. Custom foreign key\n\n// If the foreign key doesn’t follow Wheels’ naming conventions.\n// For example, Driver has one License, but the foreign key column is driver_ref.\n// In app/models/Driver.cfc\nhasOne(name="license", foreignKey="driver_ref");\n\n5. Using joinKey for non-standard PK\n\n// If the Company table uses companyCode instead of id as the primary key, and the Address table has companyCode as the foreign key:\n// In app/models/Company.cfc\nhasOne(name="address", joinKey="companyCode");\n
"},"hint":"Defines a one-to-one relationship between two models. It means each instance of this model is linked to exactly one record in another model. By default, Wheels infers table and key names, but you can customize them with arguments like foreignKey, joinKey, and joinType.\n\n","returntype":"void","slug":"model.hasOne","parameters":[{"required":true,"hint":"Gives the association a name that you refer to when working with the association (in the `include` argument to `findAll`, to name one example).","name":"name","type":"string"},{"default":"","required":false,"hint":"Name of associated model (usually not needed if you follow Wheels conventions because the model name will be deduced from the `name` argument).","name":"modelName","type":"string"},{"default":"","required":false,"hint":"Foreign key property name (usually not needed if you follow Wheels conventions since the foreign key name will be deduced from the `name` argument).","name":"foreignKey","type":"string"},{"default":"","required":false,"hint":"Column name to join to if not the primary key (usually not needed if you follow Wheels conventions since the join key will be the table's primary key/keys).","name":"joinKey","type":"string"},{"default":"outer","required":false,"hint":"Use to set the join type when joining associated tables. Possible values are `inner` (for `INNER JOIN`) and `outer` (for `LEFT OUTER JOIN`).","name":"joinType","type":"string"},{"default":false,"required":false,"hint":"Defines how to handle dependent model objects when you delete an object from this model. `delete` / `deleteAll` deletes the record(s) (`deleteAll` bypasses object instantiation). `remove` / `removeAll` sets the forein key field(s) to `NULL` (`removeAll` bypasses object instantiation).","name":"dependent","type":"string"}],"availableIn":["model"],"name":"hasOne","tags":{"categoryClass":"associationfunctions","sectionClass":"modelconfiguration","category":"Association Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with existing property\nemployee = model("employee").new();\nemployee.firstName = "Alice";\n\nwriteOutput(employee.hasProperty("firstName")); // true\n\n2. Checking a property that does not exist\nemployee = model("employee").new();\n\nwriteOutput(employee.hasProperty("middleName")); // false\n\n3. Using the dynamic helper\nemployee = model("employee").new();\nemployee.email = "alice@example.com";\n\n// Equivalent to hasProperty("email")\nif (employee.hasEmail()) {\n    writeOutput("Email property exists!");\n}\n\n4. Before using a property safely\nuser = model("user").findByKey(1);\n\n// Avoid runtime errors by checking\nif (user.hasProperty("phoneNumber")) {\n    writeOutput(user.phoneNumber);\n} else {\n    writeOutput("No phone number property defined.");\n}\n
"},"hint":"Checks if a given property exists on a model object. It’s useful for safely validating whether a field is defined before accessing it, especially in dynamic code or when working with user input. This method also provides dynamic helpers (e.g., object.hasEmail()) for convenience.\n\n","returntype":"boolean","slug":"model.hasProperty","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"hasProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with object and property\n<!--- Hidden field for user.id --->\n#hiddenField(objectName="user", property="id")#\n\n// Generates something like:\n// <input id="user-id" name="user.id" type="hidden" value="123">\n\n2. Adding extra HTML attributes\n#hiddenField(\n    objectName="user",\n    property="sessionToken",\n    id="custom-token",\n    class="hidden-tracker"\n)#\n\n// <input id="custom-token" name="user.sessionToken" type="hidden" value="abc123" class="hidden-tracker">\n\n3. Nested association (hasOne or belongsTo)\n#hiddenField(\n    objectName="order",\n    property="id",\n    association="customer"\n)#\n\n// If an order has a customer, this binds the hidden field to order.customer.id.\n\n4. Nested hasMany with position\n#hiddenField(\n    objectName="order",\n    property="id",\n    association="items",\n    position="1"\n)#\n\n// Binds to the id of the second item in the order’s items collection.\n
"},"hint":"The hiddenField() function generates a hidden <input type=\"hidden\"> tag for a given model object and property. It’s commonly used to store identifiers or other values that need to persist across form submissions without being visible to the user.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hiddenField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"hiddenField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\n#hiddenFieldTag(name="userId", value=user.id)#\n\n// Generates:\n// <input id="userId" name="userId" type="hidden" value="123">\n\n2. With additional attributes\n#hiddenFieldTag(\n    name="sessionToken",\n    value="abc123",\n    id="token-field",\n    class="hidden-tracker"\n)#\n\n// <input id="token-field" name="sessionToken" type="hidden" value="abc123" class="hidden-tracker">\n\n3. Without specifying a value (empty by default)\n#hiddenFieldTag(name="csrfToken")#\n\n// <input id="csrfToken" name="csrfToken" type="hidden" value="">\n\n4. Disabling encoding\n#hiddenFieldTag(\n    name="redirectUrl",\n    value="https://example.com/?a=1&b=2",\n    encode=false\n)#\n\n// <input id="redirectUrl" name="redirectUrl" type="hidden" value="https://example.com/?a=1&b=2">\n
"},"hint":"Generates a hidden <input type=\"hidden\"> tag using a plain name/value pair. Unlike hiddenField(), this helper does not tie to a model object — it’s meant for raw form fields where you control the name and value manually.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hiddenFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"hiddenFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage (default <span class="highlight">)\n#highlight(text="You searched for: Wheels", phrases="Wheels")#\n\n// Output:\n// You searched for: <span class="highlight">Wheels</span>\n\n2. Highlight multiple phrases\n#highlight(\n    text="ColdFusion and Wheels make development fun.",\n    phrases="ColdFusion,Wheels"\n)#\n\n// Output:\n// <span class="highlight">ColdFusion</span> and <span class="highlight">Wheels</span> make development fun.\n\n3. Use a custom delimiter for multiple phrases\n#highlight(\n    text="Apples | Oranges | Bananas",\n    phrases="Apples|Bananas",\n    delimiter="|"\n)#\n\n// Output:\n// <span class="highlight">Apples</span> | Oranges | <span class="highlight">Bananas</span>\n\n4. Use a different HTML tag\n#highlight(\n    text="Important: Read the documentation carefully.",\n    phrases="Important",\n    tag="strong"\n)#\n\n// Output:\n// <strong class="highlight">Important</strong>: Read the documentation carefully.\n
"},"hint":"Searches the given text for one or more phrases and wraps all matches in an HTML tag (default: <span>). This is useful for search results or emphasizing certain keywords dynamically.\n\n","returntype":"string","slug":"controller.highlight","parameters":[{"required":true,"hint":"Text to search in.","name":"text","type":"string"},{"required":false,"hint":"Phrase (or list of phrases) to highlight. This argument is also aliased as `phrases`.","name":"phrase","type":"string"},{"default":",","required":false,"hint":"Delimiter to use when passing in multiple phrases.","name":"delimiter","type":"string"},{"default":"span","required":false,"hint":"HTML tag to use to wrap the highlighted phrase(s).","name":"tag","type":"string"},{"default":"highlight","required":false,"hint":"Class to use in the tags wrapping highlighted phrase(s).","name":"class","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"highlight","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic 24-hour select\n#hourSelectTag(name="meetingHour")#\n\n// Output (simplified):\n// <select name="meetingHour">\n//   <option value="00">00</option>\n//   <option value="01">01</option>\n//   ...\n//   <option value="23">23</option>\n// </select>\n\n2. Pre-select an hour\n#hourSelectTag(name="meetingHour", selected="14")#\n\n// Output (simplified):\n// <option value="14" selected="selected">14</option>\n\n3. Include a blank option\n#hourSelectTag(name="meetingHour", includeBlank="- Select Hour -")#\n\n// Output (simplified):\n// <option value="">- Select Hour -</option>\n// <option value="00">00</option>\n// ...\n\n4. Use 12-hour format with AM/PM\n#hourSelectTag(name="meetingHour", twelveHour=true, selected="3")#\n\n// Output (simplified):\n// <select name="meetingHour">\n//   <option value="01">01</option>\n//   <option value="02">02</option>\n//   <option value="03" selected="selected">03</option>\n//   ...\n//   <option value="12">12</option>\n// </select>\n\n// <select name="meetingHourMeridian">\n//   <option value="AM">AM</option>\n//   <option value="PM">PM</option>\n// </select>\n
"},"hint":"Builds and returns a <select> form control for choosing an hour of the day. By default, hours are shown in 24-hour format (00–23), but you can switch to 12-hour format with an accompanying AM/PM dropdown.\n\n","returntype":"string","slug":"controller.hourSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"hourSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Humanize a string, will result in "Wheels Is A Framework"\n#humanize(text="wheelsIsAFramework")#\n\n2.Humanize a string, force wheels to replace "Cfml" with "CFML"\n#humanize(text="wheelsIsACfmlFramework", except="CFML")#
"},"hint":"Converts a camel-cased or underscored string into more readable, human-friendly text by inserting spaces and capitalizing words. You can also specify words that should be replaced or kept in a specific format.\n\n","returntype":"string","slug":"controller.humanize","parameters":[{"required":true,"hint":"Text to humanize.","name":"text","type":"string"},{"default":"","required":false,"hint":"A list of strings (space separated) to replace within the output.","name":"except","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"humanize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic camelCase string\n#hyphenize("myBlogPost")#\n\n// Output:\n// my-blog-post\n\n2. PascalCase string\n#hyphenize("UserProfileSettings")#\n\n// Output:\n// user-profile-settings\n\n3. Single word (no change)\n#hyphenize("Dashboard")#\n\n// Output:\n// dashboard\n\n4. Already hyphenated string (stays lowercase)\n#hyphenize("already-hyphenized")#\n\n// Output:\n// already-hyphenized\n
"},"hint":"Converts camelCase or PascalCase strings into lowercase hyphen-separated strings. Useful for generating URL-friendly slugs, CSS class names, or readable identifiers.\n\n","returntype":"string","slug":"controller.hyphenize","parameters":[{"required":true,"hint":"The string to hyphenize.","name":"string","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"hyphenize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Outputs an `img` tag for `public/images/logo.png`\n#imageTag("logo.png")#\n\n2. Outputs an `img` tag for `http://cfwheels.org/images/logo.png`\n#imageTag(source="http://cfwheels.org/images/logo.png", alt="ColdFusion on Wheels")#\n\n3. Outputs an `img` tag with the `class` attribute set\n#imageTag(source="logo.png", class="logo")#\n\n4. With explicit host and protocol\n#imageTag(source=\"logo.png\", onlyPath=false, host=\"cdn.myapp.com\", protocol=\"https\")#
"},"hint":"Returns an img tag.\nIf the image is stored in the local images folder, the tag will also set the width, height, and alt attributes for you.\nYou can pass any additional arguments (e.g. class, rel, id), and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.imageTag","parameters":[{"required":true,"hint":"The file name of the image if it's available in the local file system (i.e. ColdFusion will be able to access it). Provide the full URL if the image is on a remote server.","name":"source","type":"string"},{"default":true,"required":false,"name":"onlyPath","type":"boolean"},{"default":"","required":false,"name":"host","type":"string"},{"default":"","required":false,"name":"protocol","type":"string"},{"default":0,"required":false,"name":"port","type":"numeric"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"},{"default":true,"required":false,"name":"required","type":"boolean"}],"availableIn":["controller"],"name":"imageTag","tags":{"categoryClass":"assetfunctions","sectionClass":"viewhelpers","category":"Asset Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. In your view template, let's say `app/views/blog/post.cfm\ncontentFor(head='<meta name="robots" content="noindex,nofollow">');\ncontentFor(head='<meta name="author" content="wheelsdude@wheelsify.com">');\n\n// In `app/views/layout.cfm`\n<html>\n\t<head>\n\t    <title>My Site</title>\n\t    #includeContent("head")#\n\t</head>\n\t<body>\n\t\t<cfoutput>\n\t\t\t#includeContent()#\n\t\t</cfoutput>\n\t</body>\n</html>
"},"hint":"Outputs the content for a specific section in a layout. Works together with contentFor() to define and then inject content into layouts. Typically used for head, sidebar, footer, or other pluggable layout sections. If the requested section hasn’t been defined, it will either return nothing or the provided defaultValue.\n\n","returntype":"string","slug":"controller.includeContent","parameters":[{"default":"body","required":false,"hint":"Name of layout section to return content for.","name":"name","type":"string"},{"default":"","required":false,"hint":"What to display as a default if the section is not defined.","name":"defaultValue","type":"string"}],"availableIn":["controller"],"name":"includeContent","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Check to see if the customer is subscribed to the Swimsuit Edition. Note that the order of the `keys` argument should match the order of the `customerid` and `publicationid` columns in the `subscriptions` join table\nif(!includedInObject(objectName="customer", association="subscriptions", keys="#customer.key()#,#swimsuitEdition.id#")){\n    assignSalesman(customer);\n}
"},"hint":"Used as a shortcut to check if the specified IDs are a part of the main form object.\nThis method should only be used for hasMany associations.\n\n","returntype":"boolean","slug":"controller.includedInObject","parameters":[{"required":true,"hint":"Name of the variable containing the parent object to represent with this form field.","name":"objectName","type":"string"},{"required":true,"hint":"Name of the association set in the parent object to represent with this form field.","name":"association","type":"string"},{"required":true,"hint":"Primary keys associated with this form field. Note that these keys should be listed in the order that they appear in the database table.","name":"keys","type":"string"}],"availableIn":["controller"],"name":"includedInObject","tags":{"categoryClass":"formassociationfunctions","sectionClass":"viewhelpers","category":"Form Association Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Make sure that the `sidebar` value is provided for the parent layout\n<cfsavecontent variable="categoriesSidebar">\n\t<cfoutput>\n\t\t<ul>\n\t\t\t#includePartial(categories)#\n\t\t</ul>\n\t</cfoutput>\n</cfsavecontent>\ncontentFor(sidebar=categoriesSidebar);\n\n// Include parent layout at `app/views/layout.cfm`\n#includeLayout("/layout.cfm")#
"},"hint":"Includes the contents of another layout file. Typically used when a child layout wants to include a parent layout, or to nest layouts for consistent site structure.\n\n","returntype":"string","slug":"controller.includeLayout","parameters":[{"default":"layout","required":false,"hint":"Name of the layout file to include.","name":"name","type":"string"}],"availableIn":["controller"],"name":"includeLayout","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
\n1. If we're in the "sessions" controller, Wheels will include the file "app/views/sessions/_login.cfm".  \n#includePartial("login")# \n\n2. Wheels will include the file "app/views/shared/_button.cfm".  \n#includePartial(partial="/shared/button")# \n\n3. If we're in the "posts" controller and the "posts" variable includes a query result set, Wheels will loop through the record set and include the file "app/views/posts/_post.cfm" for each record.  \n<cfset posts = model("post").findAll()> \n#includePartial(posts)# \n\n4. We can also override the template file loaded for the example above.  \n#includePartial(partial="/shared/post", query=posts)# \n\n5. The same works when passing a model instance.  \n<cfset post = model("post").findByKey(params.key)> #includePartial(post)# \n#includePartial(partial="/shared/post", object=post)# \n\n6. The same works when passing an array of model objects.  \n<cfset posts = model("post").findAll(returnAs="objects")> \n#includePartial(posts)# \n#includePartial(partial="/shared/post", objects=posts)# \n</cfoutput>\n\n
"},"hint":"Includes the specified partial file in the view.\nSimilar to using cfinclude but with the ability to cache the result and use Wheels-specific file look-up.\nBy default, Wheels will look for the file in the current controller's view folder.\nTo include a file relative from the base views folder, you can start the path supplied to partial with a forward slash.\n\n","returntype":"string","slug":"controller.includePartial","parameters":[{"required":true,"hint":"The name of the partial file to be used. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Do not include the partial filename's underscore and file extension. If you want to have Wheels display the partial for a single model object, array of model objects, or a query, pass a variable containing that data into this argument.","name":"partial","type":"any"},{"default":"","required":false,"hint":"If passing a query result set for the partial argument, use this to specify the field to group the query by. A new query will be passed into the partial template for you to iterate over.","name":"group","type":"string"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"string"},{"default":"","required":false,"hint":"HTML or string to place between partials when called using a query.","name":"spacer","type":"string"},{"default":true,"required":false,"hint":"Name of controller function to load data from.","name":"dataFunction","type":"any"},{"default":true,"required":false,"name":"$prependWithUnderscore","type":"boolean"}],"availableIn":["controller"],"name":"includePartial","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single integer column age\nt.integer("age")\n\n2. Add multiple integer columns height and weight\nt.integer("height,weight")\n\n3. Add an integer column quantity with a default value of 0\nt.integer(columnNames="quantity", default=0)\n\n4. Add an integer column priority that cannot be null\nt.integer(columnNames="priority", allowNull=false)\n\n5. Add an integer column rating with a limit of 2 digits (smallint)\nt.integer(columnNames="rating", limit=2)\n\n6. Add multiple columns with different limits (comma-separated)\nt.integer(columnNames="smallValue,mediumValue,bigValue", limit=1)\n
"},"hint":"Adds one or more integer columns to a table definition during a migration. You can optionally specify a limit, default value, and whether the column allows NULL. Only available in migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.integer","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"numeric"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"integer","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. This is the method to be run inside a transaction.\npublic boolean function transferFunds(required any personFrom, required any personTo, required numeric amount) {\n\tif (arguments.personFrom.withdraw(arguments.amount) && arguments.personTo.deposit(arguments.amount)) {\n\t\treturn true;\n\t} else {\n\t\treturn false;\n\t}\n}\n\nlocal.david = model("Person").findOneByName("David");\nlocal.mary = model("Person").findOneByName("Mary");\ninvokeWithTransaction(method="transferFunds", personFrom=local.david, personTo=local.mary, amount=100);\n
"},"hint":"Runs a specified model method inside a single database transaction. This ensures that all database operations within the method are treated as a single atomic unit: either all succeed or all fail.\n\n","returntype":"any","slug":"model.invokeWithTransaction","parameters":[{"required":true,"hint":"Model method to run.","name":"method","type":"string"},{"default":"commit","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"read_committed","required":false,"hint":"Isolation level to be passed through to the cftransaction tag. See your CFML engine's documentation for more details about cftransaction's isolation attribute.","name":"isolation","type":"string"}],"availableIn":["model"],"name":"invokeWithTransaction","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Simple conditional logic\nif(isAjax()){\n    // Return JSON response for AJAX requests\n    cfcontent(type="application/json")\n    renderWith(data={ success = true, message = "This is an AJAX request" });\n} else {\n    // Render full HTML page for normal requests\n}\n\n2. Example in a Controller Action\ncomponent extends="Controller" {\n\n    function checkStatus() {\n        if (isAjax()) {\n            renderWith(data={ success = true, message = "This is an AJAX request" });\n        } else {\n            flashInsert(msg="Page loaded normally");\n            redirectTo("home");\n        }\n    }\n\n}
"},"hint":"Checks if the current request was made via JavaScript (AJAX) rather than a standard browser page load. This is useful when you want to return JSON or partial content instead of a full HTML page.\n\n","returntype":"boolean","slug":"controller.isAjax","parameters":[],"availableIn":["controller"],"name":"isAjax","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Use the passed in `id` when we're already in an instance\nfunction memberIsAdmin(){\n\tif(isClass()){\n\t\treturn this.findByKey(arguments.id).admin;\n\t} else {\n\t\treturn this.admin;\n\t}\n}\n
"},"hint":"Determines whether the method is being called at the class level (on the model itself) or on an instance of the model. This is useful when the same function can be invoked either on a model object or directly on the model class.\n\n","returntype":"string","slug":"model.isClass","parameters":[],"availableIn":["model"],"name":"isClass","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
component extends="Controller" {\n\n    public void function destroy() {\n        if (isDelete()) {\n            // Perform deletion logic\n            model("Post").deleteByKey(params.id);\n            flashInsert(success="Post deleted successfully.");\n            redirectTo(action="index");\n        } else {\n            // Handle non-DELETE request\n            flashInsert(error="Invalid request method.");\n            redirectTo(action="index");\n        }\n    }\n\n}
"},"hint":"Checks if the current HTTP request method is DELETE. This is useful for RESTful controllers where different logic is executed based on the request type.\n\n","returntype":"boolean","slug":"controller.isDelete","parameters":[],"availableIn":["controller"],"name":"isDelete","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
component extends="Controller" {\n\n    public void function show() {\n        if (isGet()) {\n            // Display a form or data\n            post = model("Post").findByKey(params.id);\n            renderWith(action="show", data=post);\n        } else {\n            // Handle non-GET request (e.g., POST, DELETE)\n            flashInsert(error="Invalid request method.");\n            redirectTo(action="index");\n        }\n    }\n\n}
"},"hint":"Checks if the current HTTP request method is GET. Useful for controlling logic depending on whether a page is being displayed or data is being requested via GET.\n\n","returntype":"boolean","slug":"controller.isGet","parameters":[],"availableIn":["controller"],"name":"isGet","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
component extends="Controller" {\n\n    public void function checkFile() {\n        if (isHead()) {\n            // Respond with headers only, no content\n        } else {\n            // Handle normal GET or other requests\n            fileData = model("File").findByKey(params.id);\n            renderWith(action="show", data=fileData);\n        }\n    }\n\n}
"},"hint":"Checks if the current HTTP request method is HEAD. HEAD requests are similar to GET requests but do not return a message body, only the headers. This is often used for checking metadata like content length or existence without transferring the actual content.\n\n","returntype":"boolean","slug":"controller.isHead","parameters":[],"availableIn":["controller"],"name":"isHead","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Use the passed in `id` when we're not already in an instance\nfunction memberIsAdmin(){\n\tif(isInstance()){\n\t\treturn this.admin;\n\t} else {\n\t\treturn this.findByKey(arguments.id).admin;\n\t}\n}\n
"},"hint":"Checks whether the current context is an instance of a model object rather than a class-level context. This is useful when a method could be called either on a class or an instance, and you want to behave differently depending on which it is.\n\n","returntype":"boolean","slug":"model.isInstance","parameters":[],"availableIn":["model"],"name":"isInstance","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Create a new object and then check if it is new (yes, this example is ridiculous. It makes more sense in the context of callbacks for example)\nemployee = model("employee").new()>\n<cfif employee.isNew()>\n    // Do something...\n</cfif>
"},"hint":"Returns true if this object hasn't been saved yet (in other words, no matching record exists in the database yet).\nReturns false if a record exists.\n\n","returntype":"boolean","slug":"model.isNew","parameters":[],"availableIn":["model"],"name":"isNew","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\nif (isOptions()) {\n   // Handle CORS preflight or respond to OPTIONS request\n   writeOutput("This is an OPTIONS request.");\n} else {\n   writeOutput("This is a different type of request.");\n}\n</cfscript>
"},"hint":"Checks whether the current HTTP request was made using the OPTIONS method. Useful in REST APIs or CORS preflight requests.\n\n","returntype":"boolean","slug":"controller.isOptions","parameters":[],"availableIn":["controller"],"name":"isOptions","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\nif (isPatch()) {\n    writeOutput("This is a PATCH request.");\n} else {\n    writeOutput("This is a different type of request.");\n}\n</cfscript>
"},"hint":"Checks whether the current HTTP request was made using the PATCH method. Useful when building RESTful APIs where PATCH is used to partially update resources.\n\n","returntype":"boolean","slug":"controller.isPatch","parameters":[],"availableIn":["controller"],"name":"isPatch","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Check an older object\nemployee = model("employee").findByKey(123);\nif (employee.isPersisted()) {\n    writeOutput("This employee exists in the database.");\n} else {\n    writeOutput("This employee has not been saved yet.");\n}\n\n2. Creating a new object\nnewEmployee = model("employee").new();\nif (!newEmployee.isPersisted()) {\n    writeOutput("This is a new object, not yet persisted.");\n}
"},"hint":"Returns true if this object has been persisted to the database or was loaded from the database via a finder.\nReturns false if the record has not been persisted to the database.\n\n","returntype":"boolean","slug":"model.isPersisted","parameters":[],"availableIn":["model"],"name":"isPersisted","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
if (isPost()) {\n    writeOutput("This request was submitted via POST.");\n} else {\n    writeOutput("This request is not a POST request.");\n}
"},"hint":"Returns whether the request came from a form POST submission or not.\n\n","returntype":"boolean","slug":"controller.isPost","parameters":[],"availableIn":["controller"],"name":"isPost","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
if (isPut()) {\n    writeOutput("This request was submitted via PUT.");\n} else {\n    writeOutput("This request is not a PUT request.");\n}
"},"hint":"Checks whether the current HTTP request is a PUT request. PUT requests are typically used to update existing resources in RESTful APIs.\n\n","returntype":"boolean","slug":"controller.isPut","parameters":[],"availableIn":["controller"],"name":"isPut","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Redirect non-secure connections to the secure version\nif (!isSecure())\n{\n\tredirectTo(protocol="https");\n}
"},"hint":"Checks whether the current request is made over a secure connection (HTTPS). Returns true if the connection is secure, otherwise false.\n\n","returntype":"boolean","slug":"controller.isSecure","parameters":[],"availableIn":["controller"],"name":"isSecure","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<!--- View Code --->\n<head>\n    <!--- Includes `public/javascripts/main.js` --->\n    #javaScriptIncludeTag("main")#\n\n    <!--- Includes `publicjavascripts/blog.js` and `public/javascripts/accordion.js` --->\n    #javaScriptIncludeTag("blog,accordion")#\n    \n    <!--- Includes external JavaScript file --->\n    #javaScriptIncludeTag("https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js")#\n</head>\n\n<body>\n    <!--- Will still appear in the `head` --->\n    #javaScriptIncludeTag(source="tabs", head=true, type=\"text/javascript\")#\n</body>\n\n
"},"hint":"Generates <script> tags for including JavaScript files. Can handle local files in the javascripts folder or external URLs. Supports multiple files and optional placement in the HTML <head>.\n\n","returntype":"string","slug":"controller.javaScriptIncludeTag","parameters":[{"default":"","required":false,"hint":"The name of one or many JavaScript files in the `javascripts` folder, minus the `.js` extension. Pass a full URL to access an external JavaScript file. Can also be called with the `source` argument.","name":"sources","type":"string"},{"default":"text/javascript","required":false,"hint":"The `type` attribute for the `script` tag.","name":"type","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to place the output in the `head` area of the HTML page instead of the default behavior (which is to place the output where the function is called from).","name":"head","type":"boolean"},{"default":",","required":false,"hint":"The delimiter to use for the list of JavaScript files.","name":"delim","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"javaScriptIncludeTag","tags":{"categoryClass":"assetfunctions","sectionClass":"viewhelpers","category":"Asset Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Single Primary Key\n\nemployee = model("employee").findByKey(42);\n\n<cfoutput>\nEmployee ID: #employee.key()# <!--- Equivalent to employee.id --->\n</cfoutput>\n\n2. Dynamic Key Retrieval\n\nanyEmployee = model("employee").findByKey(params.key);\n\nprimaryKey = anyEmployee.key();\nwriteOutput("Primary key value is: " & primaryKey);\n\n3. Composite Primary Key\n\nsubscription = model("subscription").findByKey(customerId=3, publicationId=7);\n\n<cfoutput>\nComposite Keys: #subscription.key()# <!--- Outputs: "3,7" --->\n</cfoutput>\n\n4. Use in Links or Forms\n<cfset employee = model("employee").findByKey(42)>\n\n<!--- Generate a link with dynamic primary key --->\n<a href="#linkTo(action='edit', id=employee.key())#">Edit Employee</a>\n\n<!--- Hidden field for a form --->\n#hiddenField(objectName="employee", property="id")#\n\n5. Passing Keys in Nested Relationships\n<!--- Suppose a `bookAuthors` association exists --->\nbook = model("book").findByKey(15);\n\n<cfloop array="#book.bookAuthors#" index="author">\n    <cfoutput>\n        Author Key: #author.key()# <br>\n    </cfoutput>\n</cfloop>
"},"hint":"Returns the value of the primary key for the object.\nIf you have a single primary key named id, then someObject.key() is functionally equivalent to someObject.id.\nThis method is more useful when you do dynamic programming and don't know the name of the primary key or when you use composite keys (in which case it's convenient to use this method to get a list of both key values returned).\n\n","returntype":"string","slug":"model.key","parameters":[{"default":false,"required":false,"name":"$persisted","type":"boolean"},{"default":false,"required":false,"name":"$returnTickCountWhenNew","type":"boolean"}],"availableIn":["model"],"name":"key","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
#linkTo(text="Log Out", controller="account", action="logout")#\n<!--- Ouputs: <a href="/account/logout">Log Out</a> --->\n\n<!--- If you're already in the `account` controller, Wheels will assume that's where you want the link to point --->\n#linkTo(text="Log Out", action="logout")#\n<!--- Ouputs: <a href="/account/logout">Log Out</a> --->\n\n#linkTo(text="View Post", controller="blog", action="post", key=99)#\n<!--- Ouputs: <a href="/blog/post/99">View Post</a> --->\n\n#linkTo(text="View Settings", action="settings", params="show=all&amp;sort=asc")#\n<!--- Ouputs: <a href="/account/settings?show=all&amp;amp;sort=asc">View Settings</a> --->\n\n<!--- Given that a `userProfile` route has been configured in `config/routes.cfm` --->\n#linkTo(text="Joe's Profile", route="userProfile", userName="joe")#\n<!--- Ouputs: <a href="/user/joe">Joe's Profile</a> --->\n\n<!--- Link to an external website --->\n#linkTo(text="ColdFusion Framework", href="http://cfwheels.org/")#\n<!--- Ouputs: <a href="http://cfwheels.org/">ColdFusion Framework</a> --->\n\n<!--- Give the link `class` and `id` attributes --->\n#linkTo(text="Delete Post", action="delete", key=99, class="delete", id="delete-99")#\n<!--- Ouputs: <a class="delete" href="/blog/delete/99" id="delete-99">Delete Post</a> --->\n\n
"},"hint":"Creates a link to another page in your application.\nPass in the name of a route to use your configured routes or a controller/action/key combination.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.linkTo","parameters":[{"required":false,"hint":"The text content of the link.","name":"text","type":"string"},{"default":"","required":false,"hint":"Name of a route that you have configured in config/routes.cfm.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: wheels=cool&x=y). Please note that Wheels uses the & and = characters to split the parameters and encode them properly for you. However, if you need to pass in & or = as part of the value, then you need to encode them (and only them), example: a=cats%26dogs%3Dtrouble!&b=1.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If true, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"required":false,"hint":"Pass a link to an external site here if you want to bypass the Wheels routing system altogether and link to an external URL.","name":"href","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"linkTo","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic mailto link\n#mailTo(emailAddress="webmaster@yourdomain.com")#\n<!--- Outputs: <a href="mailto:webmaster@yourdomain.com">webmaster@yourdomain.com</a> --->\n\n2. Mailto link with custom name\n#mailTo(emailAddress="webmaster@yourdomain.com", name="Contact our Webmaster")#\n<!--- Outputs: <a href="mailto:webmaster@yourdomain.com">Contact our Webmaster</a> --->\n\n3. Mailto link with special characters (encoding)\n#mailTo(emailAddress="support+help@yourdomain.com", name="Support Team")#\n<!--- Outputs: <a href="mailto:support+help@yourdomain.com">Support Team</a> --->
"},"hint":"Creates a mailto link tag to the specified email address, which is also used as the name of the link unless name is specified.\n\n","returntype":"string","slug":"controller.mailTo","parameters":[{"required":true,"hint":"The email address to link to.","name":"emailAddress","type":"string"},{"default":"","required":false,"hint":"A string to use as the link text (\"Joe\" or \"Support Department\", for example).","name":"name","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"mailTo","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Usage\n<cfscript>\nmapper()\n    .resources("posts")  // generates standard RESTful routes for posts\n    .get(name="about", pattern="about-us", to="pages##about") // custom GET route\n    .namespace("admin") // group routes under admin namespace\n        .resources("users") // RESTful routes for admin users\n    .end();\n</cfscript>\n\n2. Disable format mapping\nmapper(mapFormat=false)\n    .resources("reports");\n\n// This will prevent automatic generation of .json or .xml endpoints for the resource.
"},"hint":"Returns the mapper object used to configure your application's routes. Usually you will use this method in app/config/routes.cfm to start chaining route mapping methods like resources, namespace, etc.\n\n","returntype":"struct","slug":"controller.mapper","parameters":[{"default":true,"required":false,"hint":"Whether to turn on RESTful routing or not. Not recommended to set. Will probably be removed in a future version of wheels, as RESTful routes are the default.","name":"restful","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If not RESTful, then specify allowed routes. Not recommended to set. Will probably be removed in a future version of wheels, as RESTful routes are the default.","name":"methods","type":"boolean"},{"default":true,"required":false,"hint":"This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc. Set to false to disable automatic .[format] generation for resource based routes","name":"mapFormat","type":"boolean"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"mapper","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Maximum value for all records\nhighestSalary = model("employee").maximum("salary");\n// one-liner: highestSalary = model("employee").maximum("salary");\n\n2. Maximum value with a WHERE condition\nhighestSalary = model("employee").maximum(\n    property="salary", \n    where="departmentId=#params.departmentId#"\n);\n// one-liner: highestSalary = model("employee").maximum(property="salary", where="departmentId=#params.departmentId#");\n\n3. Maximum value with a default if no records found\nhighestSalary = model("employee").maximum(\n    property="salary", \n    where="salary > #params.minSalary#", \n    ifNull=0\n);\n// one-liner: highestSalary = model("employee").maximum(property="salary", where="salary > #params.minSalary#", ifNull=0);\n\n4. Maximum value including associations (nested join)\nhighestAlbumSales = model("album").maximum(\n    property="sales",\n    include="artist(genre)"\n);\n// one-liner: highestAlbumSales = model("album").maximum(property="sales", include="artist(genre)");\n\n5. Maximum value grouped by a column\nmaxSalaryByDept = model("employee").maximum(\n    property="salary",\n    group="departmentId"\n);\n// one-liner: maxSalaryByDept = model("employee").maximum(property="salary", group="departmentId");
"},"hint":"Calculates the maximum value for a given property.\nUses the SQL function MAX.\nIf no records can be found to perform the calculation on you can use the ifNull argument to decide what should be returned.\n\n","returntype":"any","slug":"model.maximum","parameters":[{"required":true,"hint":"Name of the property to get the highest value for (must be a property of a numeric data type).","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":true,"required":false,"name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"maximum","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Create a route like `photos/1/preview`\n    .resources(name="photos", nested=true)\n        .member()\n            .get("preview")\n        .end()\n    .end()\n.end();\n\n</cfscript>\n
"},"hint":"Scope routes within a nested resource which require use of the primary key as part of the URL pattern;\nA member route will require an ID, because it acts on a member.\nphotos/1/preview is an example of a member route, because it acts on (and displays) a single object.\n\n","returntype":"struct","slug":"mapper.member","parameters":[],"availableIn":["mapper"],"name":"member","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Migrate to a specific version\n// Returns a message with the result\nresult=application.wheels.migrator.migrateTo(version);\n
"},"hint":"Migrates the database schema to a specified version. This function is primarily intended for programmatic database migrations, but the recommended usage is via the CLI or Wheels GUI interface.\n\n","returntype":"string","slug":"migrator.migrateTo","parameters":[{"default":"","required":false,"hint":"The Database schema version to migrate to","name":"version","type":"string"}],"availableIn":["migrator"],"name":"migrateTo","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
// Migrate database to the latest version\nresult = application.wheels.migrator.migrateToLatest();\n\n// Output the result message\nwriteOutput(result);
"},"hint":"Migrates the database schema to the latest available migration version. This is a shortcut for migrateTo(version) without needing to specify a version explicitly.\n\n","returntype":"string","slug":"migrator.migrateToLatest","parameters":[],"availableIn":["migrator"],"name":"migrateToLatest","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Known Extension\n// Get the MIME type for a known extension\nmimeType = mimeTypes("jpg");\nwriteOutput(mimeType); // Outputs: "image/jpeg"\n\n2. Unknown Extension With Fallback\n// Use a fallback for unknown file types\nmimeType = mimeTypes("abc", fallback="text/plain");\nwriteOutput(mimeType); // Outputs: "text/plain"\n\n3. Dynamic Extension From User Input\nparams.type = "pdf";\nmimeType = mimeTypes(extension=params.type);\nwriteOutput(mimeType); // Outputs: "application/pdf"\n\n4. Serving a File Download\nfileName = "report.xlsx";\nfileExt = listLast(fileName, ".");\ncfheader(name="Content-Disposition", value="attachment; filename=#fileName#");\ncfcontent(type=mimeTypes(fileExt), file="#expandPath('./public/files/' & fileName)#");
"},"hint":"Returns the associated MIME type for a given file extension. Useful when serving files dynamically or setting response headers.\n\n","returntype":"string","slug":"controller.mimeTypes","parameters":[{"required":true,"hint":"The extension to get the MIME type for.","name":"extension","type":"string"},{"default":"application/octet-stream","required":false,"hint":"The fallback MIME type to return.","name":"fallback","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"mimeTypes","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Minimum Value\n// Get the lowest salary among all employees\nlowestSalary = model("employee").minimum("salary");\nwriteOutput("Lowest Salary: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum("salary"));\n\n2. Minimum Value with Condition\n// Get the lowest salary for employees in a specific department\ndeptId = 5;\nlowestSalary = model("employee").minimum(\n    property="salary",\n    where="departmentId=#deptId#"\n);\nwriteOutput("Lowest Salary in Department #deptId#: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum(property="salary", where="departmentId=5"));\n\n3. Minimum Value with Range and Fallback\n// Get the lowest salary within a range and fallback to 0 if no records\nlowestSalary = model("employee").minimum(\n    property="salary",\n    where="salary BETWEEN #params.min# AND #params.max#",\n    ifNull=0\n);\nwriteOutput("Lowest Salary in range: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum(property="salary", where="salary BETWEEN #params.min# AND #params.max#", ifNull=0));\n\n4. Including Associations\n// Get the lowest product price including related categories\nlowestPrice = model("product").minimum(\n    property="price",\n    include="category"\n);\nwriteOutput("Lowest Product Price: " & lowestPrice);\n// inline: writeOutput(model("product").minimum(property="price", include="category"));\n\n5. Include Soft-Deleted Records\n// Include soft-deleted employees in the calculation\nlowestSalary = model("employee").minimum(\n    property="salary",\n    includeSoftDeletes=true\n);\nwriteOutput("Lowest Salary including soft-deleted employees: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum(property="salary", includeSoftDeletes=true));
"},"hint":"Calculates the minimum value for a specified property in a model using SQL's MIN() function. This can be used to find the lowest value of a numeric property across all records or with conditions. You can also include associations, handle soft-deleted records, provide fallback values, and group results. If no records can be found to perform the calculation on you can use the ifNull argument to decide what should be returned.\n\n","returntype":"any","slug":"model.minimum","parameters":[{"required":true,"hint":"Name of the property to get the lowest value for (must be a property of a numeric data type).","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"minimum","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Minute Select\nminuteSelectTag(name="minuteOfMeeting", selected=params.minuteOfMeeting)\n\n2. 15-Minute Intervals\nminuteSelectTag(name="minuteOfMeeting", selected=params.minuteOfMeeting, minuteStep=15)\n\n3. Include Blank Option\nminuteSelectTag(name="minuteOfMeeting", includeBlank="- Select Minute -")\n\n4. Using Label\nminuteSelectTag(name="minuteOfMeeting", label="Select Minute")\n\n5. Custom Label Placement\nminuteSelectTag(name="minuteOfMeeting", label="Minute", labelPlacement="after")\n\n
"},"hint":"Builds and returns a <select> dropdown for the minutes of an hour (0–59). You can customize the selected value, increment steps (e.g., 5, 10, 15 minutes), label placement, and include a blank option. Useful for forms where users pick a time.\n\n","returntype":"string","slug":"controller.minuteSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"minuteSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
// The `model("author")` part of the code below gets a reference to the model from the application scope, and then the `findByKey` class level method is called on it\nauthorObject = model("author").findByKey(1);
"},"hint":"Returns a reference to a specific model defined in your application, allowing you to call class-level methods on it. This is useful when you want to access database records or invoke model methods without instantiating a new object first.\n\n","returntype":"any","slug":"controller.model","parameters":[{"required":true,"hint":"Name of the model to get a reference to.","name":"name","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"model","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\nmonthSelectTag(name="monthOfBirthday", selected=params.monthOfBirthday)\n\n2. Display months as numbers\nmonthSelectTag(name="monthOfHire", selected=3, monthDisplay="numbers")\n\n3. Display months as abbreviations\nmonthSelectTag(name="monthOfEvent", selected="Jun", monthDisplay="abbreviations")\n\n4. Include a blank option\nmonthSelectTag(name="monthOfAppointment", includeBlank="- Select Month -")\n\n5. Custom label and wrapping\nmonthSelectTag(name="monthOfSubscription", label="Subscription Month:", labelPlacement="before")\n
"},"hint":"Generates a <select> dropdown for selecting a month. You can customize its options, labels, and display format. Unlike dateSelect, this function focuses only on the month portion.\n\n","returntype":"string","slug":"controller.monthSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The month that should be selected initially.","name":"selected","type":"string"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"hint":"[see:dateSelect].","name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"hint":"[see:dateSelect].","name":"monthAbbreviations","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"monthSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    .namespace("api")\n        .namespace("v2")\n            // Route name:  apiV2Products\n            // Example URL: /api/v2/products/1234\n            // Controller:  api.v2.Products\n            .resources("products")\n        .end()\n\n        .namespace("v1")\n            // Route name:  apiV1Users\n            // Example URL: /api/v1/users\n            // Controller:  api.v1.Users\n            .get(name="users", to="users##index")\n        .end()\n    .end()\n\n    .namespace(name="foo", package="foos", path="foose")\n        // Route name:  fooBars\n        // Example URL: /foose/bars\n        // Controller:  foos.Bars\n        .post(name="bars", to="bars##create")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"The namespace() function in Wheels is used to group controllers and routes under a specific namespace (subfolder/package). It also prepends the namespace to route names and can modify the URL path. This is useful for organizing APIs, versioning, or modular applications. Namespaces can be nested for hierarchical routing, e.g., /api/v1/... and /api/v2/....\n\n","returntype":"struct","slug":"mapper.namespace","parameters":[{"required":true,"hint":"Name to prepend to child route names.","name":"name","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Subfolder (package) to reference for controllers. This defaults to the value provided for `name`.","name":"package","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Subfolder path to add to the URL.","name":"path","type":"string"}],"availableIn":["mapper"],"name":"namespace","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic nested association with auto-save\n// app/models/User.cfc\nfunction config(){\n    hasMany("groupEntitlements");\n\n    // Allow nested save of `groupEntitlements` when user is saved\n    nestedProperties(association="groupEntitlements");\n}\n\n// Controller code\nuser = model("User").findByKey(1);\nuser.groupEntitlements = [\n    {groupId=1, role="admin"},\n    {groupId=2, role="editor"}\n];\nuser.save(); \n// Both the user and nested groupEntitlements are saved automatically\n\n2. Allow deletion of nested objects\nfunction config(){\n    hasMany("groupEntitlements");\n\n    // Enable deletion via `_delete` flag\n    nestedProperties(association="groupEntitlements", allowDelete=true);\n}\n\n// Example params\nparams.user.groupEntitlements = [\n    {id=10, _delete=true},\n    {groupId=3, role="viewer"}\n];\n\nuser = model("User").findByKey(params.user.id);\nuser.setProperties(params.user);\nuser.save();\n// The first nested object (id=10) is deleted, the second is saved\n
"},"hint":"Allows nested objects, arrays, or structs associated with a model to be automatically set from incoming params or other generated data. This is particularly useful when you have hasMany or belongsTo associations and want to manage them directly when saving the parent object.\n\n","returntype":"void","slug":"model.nestedProperties","parameters":[{"default":"","required":false,"hint":"The association (or list of associations) you want to allow to be set through the params. This argument is also aliased as `associations`.","name":"association","type":"string"},{"default":true,"required":false,"hint":"Whether to save the association(s) when the parent object is saved.","name":"autoSave","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to tell Wheels to look for the property `_delete` in your model. If present and set to a value that evaluates to true, the model will be deleted when saving the parent.","name":"allowDelete","type":"boolean"},{"default":"","required":false,"hint":"Set this to a property on the object that you would like to sort by. The property should be numeric, should start with 1, and should be consecutive. Only valid with `hasMany` associations.","name":"sortProperty","type":"string"},{"default":"","required":false,"hint":"A list of properties that should not be blank. If any of the properties are blank, any CRUD operations will be rejected.","name":"rejectIfBlank","type":"string"}],"availableIn":["model"],"name":"nestedProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Create a new author in memory (not saved to the database)\nnewAuthor = model("author").new();\n\n2. Create a new author based on properties in a struct\nnewAuthor = model("author").new(params.authorStruct);\n\n3. Create a new author by passing in named arguments\nnewAuthor = model("author").new(firstName="John", lastName="Doe");\n\n4. If you have a `hasOne` or `hasMany` association setup from `customer` to `order`, you can do a scoped call. (The `newOrder` method below will call `model("order").new(customerId=aCustomer.id)` internally.)\naCustomer = model("customer").findByKey(params.customerId);\nanOrder = aCustomer.newOrder(shipping=params.shipping);\n\n5. Allow explicit timestamps\n// You can manually set createdAt and updatedAt fields\nnewAuthor = model(\"author\").new(\n    firstName=\"Bob\",\n    lastName=\"Builder\",\n    allowExplicitTimestamps=true,\n    createdAt=createDate(2025,9,24),\n    updatedAt=createDate(2025,9,24)\n);
"},"hint":"Creates a new object based on supplied properties and returns it.\nThe object is not saved to the database, it only exists in memory.\nProperty names and values can be passed in either using named arguments or as a struct to the properties argument.\n\n","returntype":"any","slug":"model.new","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to allow explicit assignment of `createdAt` or `updatedAt` properties","name":"allowExplicitTimestamps","type":"boolean"}],"availableIn":["model"],"name":"new","tags":{"categoryClass":"createfunctions","sectionClass":"modelclass","category":"Create Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Obfuscate a numeric primary key\n// Primary key value\nid = 99;\n\n// Obfuscate it before sending in the URL\nobfuscatedId = obfuscateParam(id);\nwriteOutput(obfuscatedId); \n\n2. Obfuscate a string value\n// Obfuscate an email address\nemail = "user@example.com";\nobfuscatedEmail = obfuscateParam(email);\nwriteOutput(obfuscatedEmail); \n\n3. Use obfuscated value in a link\n// Pass obfuscated ID in a linkTo helper\nuserId = 42;\n#linkTo(text="View Profile", controller="user", action="profile", key=obfuscateParam(userId))#\n
"},"hint":"Obfuscates a value, typically used to hide sensitive information like primary key IDs when passing them in URLs. This helps prevent users from easily guessing sequential IDs or sensitive values.\n\n","returntype":"string","slug":"controller.obfuscateParam","parameters":[{"required":true,"hint":"The value to obfuscate.","name":"param","type":"any"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"obfuscateParam","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Restrict an action to HTML only\nfunction show() {\n    // This action will only respond with HTML\n    onlyProvides("html");\n}\n\n2. Restrict an action to JSON and XML\nfunction data() {\n    // Only allow JSON or XML responses\n    onlyProvides("json,xml");\n}\n\n3. Override global provides setting\ncomponent extends="Controller" {\n\n    function config() {\n        // Globally allow HTML and JSON\n        provides("html,json");\n    }\n\n    function exportCsv() {\n        // Override global, allow only CSV for this action\n        onlyProvides("csv");\n\n        orders = model("order").findAll();\n    }\n}\n
"},"hint":"Use this in an individual controller action to define which formats the action will respond with.\nThis can be used to define provides behavior in individual actions or to override a global setting set with provides in the controller's config().\n\n","returntype":"void","slug":"controller.onlyProvides","parameters":[{"default":"","required":false,"hint":"Formats to instruct the controller to provide. Valid values are `html` (the default), `xml`, `json`, `csv`, `pdf`, and `xls`.","name":"formats","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Name of action, defaults to current.","name":"action","type":"string"}],"availableIn":["controller"],"name":"onlyProvides","tags":{"categoryClass":"providesfunctions","sectionClass":"controller","category":"Provides Functions","section":"Controller"}},{"extended":{"hasExtended":false,"docs":""},"hint":"This method is not designed to be called directly from your code, but provides functionality for dynamic finders such as findOneByEmail()\n\n","returntype":"any","slug":"model.onMissingMethod","parameters":[{"required":true,"name":"missingMethodName","type":"string"},{"required":true,"name":"missingMethodArguments","type":"struct"}],"availableIn":["model"],"name":"onMissingMethod","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    .package("public")\n        // Example URL: /products/1234\n        // Controller:  public.Products\n        .resources("products")\n    .end()\n\n    // Example URL: /users/4321\n    // Controller:  Users\n    .resources(name="users", nested=true)\n        // Calling `package` here is useful to scope nested routes for the `users`\n        // resource into a subfolder.\n        .package("users")\n            // Example URL: /users/4321/profile\n            // Controller:  users.Profiles\n            .resource("profile")\n        .end()\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Scopes the controllers for any routes defined inside its block to a specific subfolder (package) without adding the package name to the URL. This is useful for organizing your controllers in subfolders while keeping the URL structure clean.\n\n","returntype":"struct","slug":"mapper.package","parameters":[{"required":true,"hint":"Name to prepend to child route names.","name":"name","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Subfolder (package) to reference for controllers. This defaults to the value provided for `name`.","name":"package","type":"string"}],"availableIn":["mapper"],"name":"package","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
component extends="app.tests.Test" {\n\n    function packageSetup() {\n        // Run once before any test in this package\n        \n        // Create a test user\n        model("user").new(username="testuser", email="test@example.com").save();\n\n        // Initialize test data\n        application.testConfig = {\n            siteName: "Wheels Test"\n        };\n    }\n\n    function test_User_Creation() {\n        var user = model("user").findOneByUsername("testuser");\n        assert("user eq true");\n    }\n\n    function test_Config_Value() {\n        assert("application.testConfig.siteName eq 'Wheels Test'");\n    }\n}\n
"},"hint":"The packageSetup() function is a callback in Wheels’ legacy testing framework. It runs once before the first test case in the test package. Use it to perform setup tasks that are shared across all tests in the package, such as initializing data, creating test records, or configuring environment settings.\n\n","returntype":"any","slug":"test.packageSetup","parameters":[],"availableIn":["test"],"name":"packageSetup","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
component extends="app.tests.Test" {\n\n    function packageSetup() {\n        // Run once before any test in this package\n        model("user").new(username="testuser", email="test@example.com").save();\n    }\n\n    function packageTeardown() {\n        // Run once after all tests in this package\n\n        // Delete test user\n        var user = model("user").findOneByUsername("testuser");\n        if (user) {\n            user.delete();\n        }\n\n        // Clear test configuration\n        structClear(application.testConfig);\n    }\n\n    function test_User_Exists() {\n        var user = model("user").findOneByUsername("testuser");\n        assert("user eq true");\n    }\n}\n
"},"hint":"The packageTeardown() function is a callback in Wheels’ legacy testing framework. It runs once after the last test case in the test package. Use it to perform cleanup tasks that are shared across all tests in the package, such as deleting test records, resetting application state, or clearing cached data.\n\n","returntype":"any","slug":"test.packageTeardown","parameters":[],"availableIn":["test"],"name":"packageTeardown","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
allAuthors = model("author").findAll(page=1, perPage=25, order="lastName", handle="authorsData");\npaginationData = pagination("authorsData");\n\n#pagination().currentPage#\n#pagination().totalPages#\n#pagination().totalRecords#
"},"hint":"Returns a struct with information about the specificed paginated query.\nThe keys that will be included in the struct are currentPage, totalPages and totalRecords.\n\n","returntype":"struct","slug":"controller.pagination","parameters":[{"default":"query","required":false,"hint":"The handle given to the query to return pagination information for.","name":"handle","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"pagination","tags":{"categoryClass":"paginationfunctions","sectionClass":"controller","category":"Pagination Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
//--------------------------------------------------------------------\n// Example 1: List authors page by page, 25 at a time\n\n// Controller code\nparam name="params.page" type="integer" default="1";\nauthors = model("author").findAll(page=params.page, perPage=25, order="lastName");\n\n// View code\n<ul>\n    <cfoutput query="authors">\n        <li>#EncodeForHtml(firstName)# #EncodeForHtml(lastName)#</li>\n    </cfoutput>\n</ul>\n\n<cfoutput>#paginationLinks(route="authors")#</cfoutput>\n\n\n//--------------------------------------------------------------------\n// Example 2: Using the same model call above, show all authors with a\n// window size of 5\n\n// View code\n<cfoutput>#paginationLinks(route="authors", windowSize=5)#</cfoutput>\n\n\n//--------------------------------------------------------------------\n// Example 3: If more than one paginated query is being run, then you\n// need to reference the correct `handle` in the view\n\n// Controller code\nauthors = model("author").findAll(handle="authQuery", page=5, order="id");\n\n// View code\n<ul>\n    <cfoutput>\n        #paginationLinks(\n            route="authors",\n            handle="authQuery",\n            prependToLink="<li>",\n            appendToLink="</li>"\n        )#\n    </cfoutput>\n</ul>\n\n\n//--------------------------------------------------------------------\n// Example 4: Call to `paginationLinks` using routes\n\n// Route setup in config/routes.cfm\nmapper()\n    .get(name="paginatedCommentListing", pattern="blog/[year]/[month]/[day]/[page]", to="blogs##stats")\n    .get(name="commentListing", pattern="blog/[year]/[month]/[day]", to="blogs##stats")\n.end();\n\n// Controller code\nparam name="params.page" type="integer" default="1";\ncomments = model("comment").findAll(page=params.page, order="createdAt");\n\n// View code\n<ul>\n    <cfoutput>\n        #paginationLinks(\n            route="paginatedCommentListing",\n            year=2009,\n            month="feb",\n            day=10\n        )#\n    </cfoutput>\n</ul>\n
"},"hint":"Builds and returns a string containing links to pages based on a paginated query.\nUses linkTo() internally to build the link, so you need to pass in a route name or a controller/action/key combination.\nAll other linkTo() arguments can be supplied as well, in which case they are passed through directly to linkTo().\nIf you have paginated more than one query in the controller, you can use the handle argument to reference them. (Don't forget to pass in a handle to the findAll() function in your controller first.)\n\n","returntype":"string","slug":"controller.paginationLinks","parameters":[{"default":2,"required":false,"hint":"The number of page links to show around the current page.","name":"windowSize","type":"numeric"},{"default":true,"required":false,"hint":"Whether or not links to the first and last page should always be displayed.","name":"alwaysShowAnchors","type":"boolean"},{"default":" ... ","required":false,"hint":"String to place next to the anchors on either side of the list.","name":"anchorDivider","type":"string"},{"default":false,"required":false,"hint":"Whether or not the current page should be linked to.","name":"linkToCurrentPage","type":"boolean"},{"default":"","required":false,"hint":"String or HTML to be prepended before result.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String or HTML to be appended after result.","name":"append","type":"string"},{"default":"","required":false,"hint":"String or HTML to be prepended before each page number.","name":"prependToPage","type":"string"},{"default":false,"required":false,"name":"addActiveClassToPrependedParent","type":"boolean"},{"default":true,"required":false,"hint":"Whether or not to prepend the prependToPage string on the first page in the list.","name":"prependOnFirst","type":"boolean"},{"default":true,"required":false,"hint":"Whether or not to prepend the prependToPage string on the anchors.","name":"prependOnAnchor","type":"boolean"},{"default":"","required":false,"hint":"String or HTML to be appended after each page number.","name":"appendToPage","type":"string"},{"default":true,"required":false,"hint":"Whether or not to append the appendToPage string on the last page in the list.","name":"appendOnLast","type":"boolean"},{"default":true,"required":false,"hint":"Whether or not to append the appendToPage string on the anchors.","name":"appendOnAnchor","type":"boolean"},{"default":"","required":false,"hint":"Class name for the current page number (if linkToCurrentPage is true, the class name will go on the a element. If not, a span element will be used).","name":"classForCurrent","type":"string"},{"default":"query","required":false,"hint":"The handle given to the query that the pagination links should be displayed for.","name":"handle","type":"string"},{"default":"page","required":false,"hint":"The name of the param that holds the current page number.","name":"name","type":"string"},{"default":false,"required":false,"hint":"Will show a single page when set to true. (The default behavior is to return an empty string when there is only one page in the pagination).","name":"showSinglePage","type":"boolean"},{"default":true,"required":false,"hint":"Decides whether to link the page number as a param or as part of a route. (The default behavior is true).","name":"pageNumberAsParam","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"paginationLinks","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Password Field\n<cfoutput>\n    #passwordField(label="Password", objectName="user", property="password")#\n</cfoutput>\n\n2. Password Field for a Nested Association\n<fieldset>\n    <legend>Passwords</legend>\n    <cfloop from="1" to="#ArrayLen(user.passwords)#" index="i">\n        #passwordField(\n            label="Password ##i#", \n            objectName="user", \n            association="passwords", \n            position=i, \n            property="password"\n        )#\n    </cfloop>\n</fieldset>\n\n3. Custom Label Placement and Error Handling\n<cfoutput>\n    #passwordField(\n        label="Enter Your Password",\n        objectName="user",\n        property="password",\n        labelPlacement="before",\n        errorClass="input-error",\n        prepend="<div class='input-group'>",\n        append="</div>"\n    )#\n</cfoutput>\n
"},"hint":"Builds and returns a string containing a password field form control based on the supplied objectName and property.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.passwordField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"passwordField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Password Field\n<cfoutput>\n    #passwordFieldTag(label="Password", name="password", value="")#\n</cfoutput>\n\n2. Label Placement Before Input\n<cfoutput>\n    #passwordFieldTag(label="Password", name="password", labelPlacement="before")#\n</cfoutput>\n\n3. Wrapping Input with Custom HTML\n<cfoutput>\n    #passwordFieldTag(\n        label="Enter Password",\n        name="password",\n        prepend="<div class='input-group'>",\n        append="</div>"\n    )#\n</cfoutput>\n\n4. Custom Label Decoration\n<cfoutput>\n    #passwordFieldTag(\n        label="Password",\n        name="password",\n        prependToLabel="<strong>",\n        appendToLabel="</strong>"\n    )#\n</cfoutput>\n
"},"hint":"Builds and returns a string containing a password field form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.passwordFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"passwordFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  ghostStory\n    // Example URL: /ghosts/666/stories/616\n    // Controller:  Stories\n    // Action:      update\n    .patch(name="ghostStory", pattern="ghosts/[ghostKey]/stories/[key]", to="stories##update")\n\n    // Route name:  goblins\n    // Example URL: /goblins\n    // Controller:  Goblins\n    // Action:      update\n    .patch(name="goblins", controller="goblins", action="update")\n\n    // Route name:  heartbeat\n    // Example URL: /heartbeat\n    // Controller:  Sessions\n    // Action:      update\n    .patch(name="heartbeat", to="sessions##update")\n\n    // Route name:  usersPreferences\n    // Example URL: /preferences\n    // Controller:  users.Preferences\n    // Action:      update\n    .patch(name="preferences", to="preferences##update", package="users")\n\n    // Route name:  orderShipment\n    // Example URL: /shipments/5432\n    // Controller:  orders.Shipments\n    // Action:      update\n    .patch(\n        name="shipment",\n        pattern="shipments/[key]",\n        to="shipments##update",\n        package="orders"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="subscribers", nested=true)\n        // Route name:  launchSubscribers\n        // Example URL: /subscribers/3209/launch\n        // Controller:  Subscribers\n        // Action:      launch\n        .patch(name="launch", to="subscribers##update", on="collection")\n\n        // Route name:  discontinueSubscriber\n        // Example URL: /subscribers/2251/discontinue\n        // Controller:  Subscribers\n        // Action:      discontinue\n        .patch(name="discontinue", to="subscribers##discontinue", on="member")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Create a route that matches a URL requiring an HTTP PATCH method. We recommend using this matcher to expose actions that update database records.\n\n","returntype":"struct","slug":"mapper.patch","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"patch","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Check if a specific plugin is installed\n<cfif ListFindNoCase("scaffold", pluginNames())>\n    <cfoutput>\n        The Scaffold plugin is installed!\n    </cfoutput>\n<cfelse>\n    <cfoutput>\n        Scaffold plugin is not installed.\n    </cfoutput>\n</cfif>\n\n2. List all installed plugins\n<cfoutput>\nInstalled Plugins: #pluginNames()#\n</cfoutput>\n\n3. Loop through all installed plugins\n<cfloop list="#pluginNames()#" index="plugin">\n    <cfoutput>\n        Plugin: #plugin#<br>\n    </cfoutput>\n</cfloop>\n\n4. Conditional logic based on multiple plugins\n<cfset plugins = pluginNames()>\n\n<cfif ListFindNoCase("scaffold", plugins) AND ListFindNoCase("seo", plugins)>\n    <cfoutput>\n        Both Scaffold and SEO plugins are installed.\n    </cfoutput>\n</cfif>\n
"},"hint":"Returns a list of all installed Wheels plugins in your application. This can be useful if you want to check for the presence of a plugin before calling its functionality, or to display available plugins dynamically.\n\n","returntype":"string","slug":"controller.pluginNames","parameters":[],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"pluginNames","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic pluralization\npluralize("person")\n<!--- Returns: "people" --->\n\n2. Pluralization with count (count = 1, so singular is returned)\npluralize(word="car", count=1)\n<!--- Returns: "1 car" --->\n\n3. Pluralization with count (count = 5, so plural is returned)\npluralize(word="car", count=5)\n<!--- Returns: "5 cars" --->\n\n4. Suppressing the count in the result\npluralize(word="dog", count=3, returnCount=false)\n<!--- Returns: "dogs" --->\n\n5. Irregular plural (child → children)\npluralize("child")\n<!--- Returns: "children" --->\n\n6. Uncountable word stays the same\npluralize("equipment")\n<!--- Returns: "equipment" --->\n\n7. With count and uncountable word\npluralize(word="equipment", count=2)\n<!--- Returns: "2 equipment" --->\n
"},"hint":"Returns the plural form of the passed in word. Can also pluralize a word based on a value passed to the count argument. Wheels stores a list of words that are the same in both singular and plural form (e.g. \"equipment\", \"information\") and words that don't follow the regular pluralization rules (e.g. \"child\" / \"children\", \"foot\" / \"feet\"). Use get(\"uncountables\") / set(\"uncountables\", newList) and get(\"irregulars\") / set(\"irregulars\", newList) to modify them to suit your needs.\n\n","returntype":"string","slug":"controller.pluralize","parameters":[{"required":true,"hint":"The word to pluralize.","name":"word","type":"string"},{"default":"-1","required":false,"hint":"Pluralization will occur when this value is not 1.","name":"count","type":"numeric"},{"default":"true","required":false,"hint":"Will return count prepended to the pluralization when true and count is not -1.","name":"returnCount","type":"boolean"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"pluralize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  widgets\n    // Example URL: /sites/918/widgets\n    // Controller:  Widgets\n    // Action:      create\n    .post(name="widgets", pattern="sites/[siteKey]/widgets", to="widgets##create")\n\n    // Route name:  wadgets\n    // Example URL: /wadgets\n    // Controller:  Wadgets\n    // Action:      create\n    .post(name="wadgets", controller="wadgets", action="create")\n\n    // Route name:  authenticate\n    // Example URL: /oauth/token.json\n    // Controller:  Tokens\n    // Action:      create\n    .post(name="authenticate", pattern="oauth/token.json", to="tokens##create")\n\n    // Route name:  usersPreferences\n    // Example URL: /preferences\n    // Controller:  users.Preferences\n    // Action:      create\n    .post(name="preferences", to="preferences##create", package="users")\n\n    // Route name:  extranetOrders\n    // Example URL: /buy-now/orders\n    // Controller:  extranet.Orders\n    // Action:      create\n    .post(\n        name="orders",\n        pattern="buy-now/orders",\n        to="orders##create",\n        package="extranet"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="customers", nested=true)\n        // Route name:  leadsCustomers\n        // Example URL: /customers/leads\n        // Controller:  Leads\n        // Action:      create\n        .post(name="leads", to="leads##create", on="collection")\n\n        // Route name:  cancelCustomer\n        // Example URL: /customers/3209/cancel\n        // Controller:  Cancellations\n        // Action:      create\n        .post(name="cancel", to="cancellations##create", on="member")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Create a route that matches a URL requiring an HTTP POST method. We recommend using this matcher to expose actions that create database records.\n\n","returntype":"struct","slug":"mapper.post","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPosts`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPosts` generates a pattern of `blog-posts`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"post","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Get the primary key of a simple table\n// For employees table with id as primary key\nkeyName = model("employee").primaryKey();\n// Returns: "id"\n\n2. Alias usage\nkeyName = model("employee").primaryKeys();\n// Returns: "id"\n\n3. Composite primary key table (e.g., order_products with order_id + product_id)\nkeys = model("orderProduct").primaryKey();\n// Returns: "order_id,product_id"\n\n4. Fetching just the first key in a composite set\nfirstKey = model("orderProduct").primaryKey(position=1);\n// Returns: "order_id"\n\n5. Fetching the second key in a composite set\nsecondKey = model("orderProduct").primaryKey(position=2);\n// Returns: "product_id"\n
"},"hint":"Returns the name of the primary key column for the table mapped to a given model. Wheels determines this automatically by introspecting the database. If the table uses a single primary key, the function returns that key’s name as a string. For tables with composite primary keys, the function will return a list of all keys. You can optionally pass in the position argument to retrieve a specific key from a composite set. This function is also available as the alias primaryKeys().\n\n","returntype":"string","slug":"model.primaryKey","parameters":[{"default":0,"required":false,"hint":"If you are accessing a composite primary key, pass the position of a single key to fetch.","name":"position","type":"numeric"}],"availableIn":["model"],"name":"primaryKey","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic auto-incrementing integer primary key\nt.primaryKey(name="id", autoIncrement=true);\n\n2. Primary key with custom type\nt.primaryKey(name="sku", type="string", limit=20);\n\n3. Composite primary keys (order_id + product_id)\nt.primaryKey(name="order_id", type="integer");\nt.primaryKey(name="product_id", type="integer");\n\n4. UUID primary key\nt.primaryKey(name="session_id", type="uuid");\n\n5. Primary key with foreign key reference\nt.primaryKey(name="payment_id", autoIncrement=true);\n
"},"hint":"Used inside migration table definitions to define a primary key for the table. By default, it creates a single-column integer primary key, but you can customize the data type, size, precision, and whether it should auto-increment. If you need composite primary keys, you can call this method multiple times within the same table definition. Additionally, you can configure references to other tables, along with cascading behaviors for updates and deletes. Only available in the migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.primaryKey","parameters":[{"required":true,"name":"name","type":"string"},{"default":"integer","required":false,"name":"type","type":"string"},{"default":"false","required":false,"name":"autoIncrement","type":"boolean"},{"required":false,"name":"limit","type":"numeric"},{"required":false,"name":"precision","type":"numeric"},{"required":false,"name":"scale","type":"numeric"},{"required":false,"name":"references","type":"string"},{"default":"","required":false,"name":"onUpdate","type":"string"},{"default":"","required":false,"name":"onDelete","type":"string"}],"availableIn":["tabledefinition"],"name":"primaryKey","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get the primary key of a simple table\n// Employees table has "id" as primary key\nkeyNames = model("employee").primaryKeys();\n// Returns: "id"\n\n2. Composite primary keys (order_products table with order_id + product_id)\nkeys = model("orderProduct").primaryKeys();\n// Returns: "order_id,product_id"\n\n3. Get only the first key in a composite primary key\nfirstKey = model("orderProduct").primaryKeys(position=1);\n// Returns: "order_id"\n\n4. Get only the second key in a composite primary key\nsecondKey = model("orderProduct").primaryKeys(position=2);\n// Returns: "product_id"\n\n5. Using alias for clarity in multi-key situations\n// This makes it more obvious the table has multiple keys\nkeys = model("orderProduct").primaryKeys();\n// Easier to read than using model("orderProduct").primaryKey()\n
"},"hint":"Alias for primaryKey().\nUse this for better readability when you're accessing multiple primary keys.\n\n","returntype":"string","slug":"model.primaryKeys","parameters":[{"default":0,"required":false,"hint":"If you are accessing a composite primary key, pass the position of a single key to fetch.","name":"position","type":"numeric"}],"availableIn":["model"],"name":"primaryKeys","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Run an action with default behavior (all filters applied)\nresult = processAction("show");\n// Executes the "show" action of the current controller with before/after filters\n\n2. Run an action but only apply "before" filters\nresult = processAction("edit", includeFilters="before");\n// Useful for testing preconditions without running the full action\n\n3. Run an action but only apply "after" filters\nresult = processAction("update", includeFilters="after");\n// Useful for testing cleanup logic that runs post-action\n\n4. Run an action without any filters\nresult = processAction("delete", includeFilters=false);\n// Skips before/after filters, only executes the "delete" action\n\n5. Simulating in a test case\nit("should process the show action without filters", function() {\n    var controller = controller("users");\n    var success = controller.processAction("show", includeFilters=false);\n    expect(success).toBeTrue();\n});\n
"},"hint":"Process the specified action of the controller.\nThis is exposed in the API primarily for testing purposes; you would not usually call it directly unless in the test suite. The optional includeFilters argument allows you to control whether before filters, after filters, or no filters at all should run when invoking the action. By default, all filters execute unless explicitly restricted.\n\n","returntype":"boolean","slug":"controller.processAction","parameters":[{"default":true,"required":false,"hint":"Set to `before` to only execute \"before\" filters, `after` to only execute \"after\" filters or `false` to skip all filters. This argument is generally inherited from the `processRequest` function during unit test execution.","name":"includeFilters","type":"string"}],"availableIn":["controller"],"name":"processAction","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Simple request, returns rendered output as string\nresult = processRequest(params={controller="users", action="show", id=5});\n// Returns: rendered HTML for the users/show action\n\n2. Simulate a POST request\nresult = processRequest(\n    params={controller="users", action="create", name="Alice"},\n    method="post"\n);\n// Returns: rendered output of the create action\n\n3. Get a detailed struct response instead of just body\nresult = processRequest(\n    params={controller="sessions", action="create", email="test@example.com"},\n    method="post",\n    returnAs="struct"\n);\n// Returns struct with keys: body, emails, files, flash, redirect, status, type\n\n4. Automatically roll back database changes\nresult = processRequest(\n    params={controller="orders", action="create", product_id=42},\n    method="post",\n    rollback=true\n);\n// Data is inserted during the request but rolled back afterward\n\n5. Skip all filters\nresult = processRequest(\n    params={controller="users", action="delete", id=10},\n    includeFilters=false\n);\n// Runs delete action without before/after filters\n\n6. Run only "before" filters (useful for testing filter logic)\nresult = processRequest(\n    params={controller="users", action="edit", id=10},\n    includeFilters="before"\n);\n
"},"hint":"Creates a controller and calls an action on it.\nWhich controller and action that's called is determined by the params passed in.\nReturns the result of the request either as a string or in a struct with body, emails, files, flash, redirect, status, and type.\nPrimarily used for testing purposes.\n\n","returntype":"any","slug":"controller.processRequest","parameters":[{"required":true,"hint":"The params struct to use in the request (make sure that at least `controller` and `action` are set).","name":"params","type":"struct"},{"default":"get","required":false,"hint":"The HTTP method to use in the request (`get`, `post` etc).","name":"method","type":"string"},{"default":"","required":false,"hint":"Pass in `struct` to return all information about the request instead of just the final output (`body`).","name":"returnAs","type":"string"},{"default":false,"required":false,"hint":"Pass in `true` to roll back all database transactions made during the request.","name":"rollback","type":"string"},{"default":true,"required":false,"hint":"Set to `before` to only execute \"before\" filters, `after` to only execute \"after\" filters or `false` to skip all filters.","name":"includeFilters","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"processRequest","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get all properties for a user object\nuser = model("user").findByKey(1);\nprops = user.properties();\n\n2. Exclude nested/associated properties\nuser = model("user").findByKey(1);\nprops = user.properties(returnIncluded=false);\n\n3. Iterate through properties\nuser = model("user").findByKey(2);\nprops = user.properties();\nfor (key in props) {\n    writeOutput("#key#: #props[key]#<br>");\n}\n\n4. Convert properties to JSON for API output\nuser = model("user").findByKey(3);\nprops = user.properties(returnIncluded=false);\njsonData = serializeJSON(props);\n
"},"hint":"Returns a structure containing all the properties of a model object, where the keys are the property (column) names and the values are the current values for that object. This is useful when you want to inspect all the attributes of a record at once, serialize data, or debug object state. By default, properties() includes nested or associated properties (such as related objects). You can control this behavior using the returnIncluded argument to exclude them if you only want the direct properties of the object.\n\n","returntype":"struct","slug":"model.properties","parameters":[{"default":true,"required":false,"hint":"Whether to return nested properties or not.","name":"returnIncluded","type":"boolean"}],"availableIn":["model"],"name":"properties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Tell Wheels that when we are referring to `firstName` in the CFML code, it should translate to the `STR_USERS_FNAME` column when interacting with the database instead of the default (which would be the `firstname` column)\nproperty(name="firstName", column="STR_USERS_FNAME");\n\n2. Tell Wheels that when we are referring to `fullName` in the CFML code, it should concatenate the `STR_USERS_FNAME` and `STR_USERS_LNAME` columns\nproperty(name="fullName", sql="STR_USERS_FNAME + ' ' + STR_USERS_LNAME");\n\n3. Tell Wheels that when displaying error messages or labels for form fields, we want to use `First name(s)` as the label for the `STR_USERS_FNAME` column\nproperty(name="firstName", label="First name(s)");\n\n4. Tell Wheels that when creating new objects, we want them to be auto-populated with a `firstName` property of value `Dave`\nproperty(name="firstName", defaultValue="Dave");\n\n5. Exclude property from SELECT queries\n// Useful for virtual/computed properties you don’t want fetched from the DB\nproperty(name=\"tempValue\", select=false);\n\n6. Override data type explicitly\nproperty(name=\"isActive\", dataType=\"boolean\");\n
"},"hint":"Lets you customize how model properties map to database columns or SQL expressions. By default, Wheels automatically maps a model’s property name to the column with the same name in the table. However, when your database uses non-standard column names, calculated values, or requires custom behavior, you can use property() to override the default mapping.\n\n","returntype":"void","slug":"model.property","parameters":[{"required":true,"hint":"The name that you want to use for the column or SQL function result in the CFML code.","name":"name","type":"string"},{"default":"","required":false,"hint":"The name of the column in the database table to map the property to.","name":"column","type":"string"},{"default":"","required":false,"hint":"An SQL expression to use to calculate the property value.","name":"sql","type":"string"},{"default":"","required":false,"hint":"A custom label for this property to be referenced in the interface and error messages.","name":"label","type":"string"},{"required":false,"hint":"A default value for this property.","name":"defaultValue","type":"string"},{"default":"true","required":false,"hint":"Whether to include this property by default in SELECT statements","name":"select","type":"boolean"},{"default":"char","required":false,"hint":"Specify the column dataType for this property","name":"dataType","type":"string"},{"required":false,"hint":"Enable / disable automatic validations for this property.","name":"automaticValidations","type":"boolean"}],"availableIn":["model"],"name":"property","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\nuser = model("user").new();\nisBlank = user.propertyIsBlank("firstName"); // returns true if firstName is not set\n\n2. Property exists but is empty\nuser = model("user").new(firstName="");\nisBlank = user.propertyIsBlank("firstName"); // true\n\n3. Property exists with value\nuser = model("user").new(firstName="Joe");\nisBlank = user.propertyIsBlank("firstName"); // false\n\n4. Checking property that doesn’t exist on the model\nisBlank = user.propertyIsBlank("nonexistentProperty"); // true\n\n5. Using in validation logic\nif (user.propertyIsBlank("email")) {\n    writeOutput("Email is required.");\n}\n
"},"hint":"Returns true if the specified property doesn't exist on the model or is an empty string.\nThis method is the inverse of propertyIsPresent().\n\n","returntype":"boolean","slug":"model.propertyIsBlank","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"propertyIsBlank","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Property exists with a value\nemployee = model("employee").new();\nemployee.firstName = "Dude";\nwriteOutput(employee.propertyIsPresent("firstName")); // true\n\n2. Property exists but is blank\nemployee.firstName = "";\nwriteOutput(employee.propertyIsPresent("firstName")); // false\n\n3. Property does not exist on the model\nwriteOutput(employee.propertyIsPresent("nonexistentProperty")); // false\n\n4. Conditional logic\nif (!employee.propertyIsPresent("email")) {\n    writeOutput("Email is required.");\n}\n
"},"hint":"Returns true if the specified property exists on the model and is not a blank string. This is the inverse of propertyIsBlank() which checks that a property is either missing or empty.\n\n","returntype":"boolean","slug":"model.propertyIsPresent","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"propertyIsPresent","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Get property names for the User model\npropNames = model("user").propertyNames();\nwriteOutput(propNames);\n\n2. Loop through property names\nfor (prop in listToArray(model("employee").propertyNames())) {\n    writeOutput("Property: #prop#<br>");\n}\n\n3. Check if a property exists in the list\nif (listFindNoCase(model("order").propertyNames(), "totalAmount")) {\n    writeOutput("Order model has a totalAmount property.");\n}\n\n4. Including calculated properties\n// In the model configuration:\nproperty(name="fullName", sql="firstName + ' ' + lastName");\n\n// propertyNames() will now include "fullName"\nwriteOutput(model("user").propertyNames());\n
"},"hint":"Returns a list of all property names associated with a model. The list is ordered by the columns’ ordinal positions as they exist in the underlying database table. In addition to actual table columns, the list also includes any calculated properties defined through the property(), method, which may be derived from SQL expressions or mapped column names. This is useful when you need to dynamically work with all of a model’s attributes without hardcoding them, such as generating dynamic forms, building custom serializers, or inspecting ORM mappings.\n\n","returntype":"string","slug":"model.propertyNames","parameters":[],"availableIn":["model"],"name":"propertyNames","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
// In `app/models/User.cfc`, `firstName` and `lastName` cannot be changed through mass assignment operations like `updateAll()`.\nfunction config(){\n\tprotectedProperties("firstName,lastName");\n}\n
"},"hint":"Used to protect one or more model properties from being set or modified through mass assignment operations. Mass assignment occurs when values are assigned to a model in bulk, such as through create(), update(), or updateAll() using a struct of data. By marking certain properties as protected, you can prevent accidental or malicious changes to sensitive fields (such as id, role, or passwordHash). This method is typically called in the model’s config() function to define rules that apply across the entire model.\n\n","returntype":"void","slug":"model.protectedProperties","parameters":[{"default":"","required":false,"hint":"Property name (or list of property names) that are not allowed to be altered through mass assignment.","name":"properties","type":"string"}],"availableIn":["model"],"name":"protectedProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Protect all POST requests globally\n// In app/controllers/Controller.cfc\nfunction config() {\n    protectsFromForgery();\n}
"},"hint":"Tells Wheels to protect POSTed requests from CSRF vulnerabilities.\nInstructs the controller to verify that params.authenticityToken or X-CSRF-Token HTTP header is provided along with the request containing a valid authenticity token.\nCall this method within a controller's config method, preferably the base Controller.cfc file, to protect the entire application.\n\n","returntype":"any","slug":"controller.protectsFromForgery","parameters":[{"default":"exception","required":false,"hint":"How to handle invalid authenticity token checks. Valid values are `error` (throws a `Wheels.InvalidAuthenticityToken` error), `abort` (aborts the request silently and sends a blank response to the client), and `ignore` (ignores the check and lets the request proceed).","name":"with","type":"string"},{"default":"","required":false,"hint":"List of actions that this check should only run on. Leave blank for all.","name":"only","type":"string"},{"default":"","required":false,"hint":"List of actions that this check should be omitted from running on. Leave blank for no exceptions.","name":"except","type":"string"}],"availableIn":["controller"],"name":"protectsFromForgery","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Provide HTML, XML, and JSON responses\nfunction config() {\n    provides("html,xml,json");\n}\n\n2. Provide only JSON and CSV\nfunction config() {\n    provides("json,csv");\n}\n\n3. Default behavior (HTML only)\nfunction config() {\n    provides(); // equivalent to provides("html")\n}\n\n4. Handling requested format in the action\nfunction show() {\n    // Wheels automatically detects the requested format and renders accordingly\n    renderwith(data=model("user").findByKey(params.id));\n}\n
"},"hint":"The `provides()` function defines the response formats that a controller can return. Clients can request a specific format in three ways: by using a URL parameter called `format` (e.g., `?format=json`), by appending the format as an extension to the URL (e.g., `/users/1.json`) when URL rewriting is enabled, or by specifying the desired format in the `Accept` header of the HTTP request. By defining the supported formats, you ensure that your controller can automatically render the response in the requested format, such as HTML, JSON, XML, CSV, PDF, or XLS. If no format is requested or supported, the controller defaults to HTML.\n\n","returntype":"void","slug":"controller.provides","parameters":[{"default":"","required":false,"hint":"Formats to instruct the controller to provide. Valid values are `html` (the default), `xml`, `json`, `csv`, `pdf`, and `xls`.","name":"formats","type":"string"}],"availableIn":["controller"],"name":"provides","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  ghostStory\n    // Example URL: /ghosts/666/stories/616\n    // Controller:  Stories\n    // Action:      update\n    .put(name="ghostStory", pattern="ghosts/[ghostKey]/stories/[key]", to="stories##update")\n\n    // Route name:  goblins\n    // Example URL: /goblins\n    // Controller:  Goblins\n    // Action:      update\n    .put(name="goblins", controller="goblins", action="update")\n\n    // Route name:  heartbeat\n    // Example URL: /heartbeat\n    // Controller:  Sessions\n    // Action:      update\n    .put(name="heartbeat", to="sessions##update")\n\n    // Route name:  usersPreferences\n    // Example URL: /preferences\n    // Controller:  users.Preferences\n    // Action:      update\n    .put(name="preferences", to="preferences##update", package="users")\n\n    // Route name:  orderShipment\n    // Example URL: /shipments/5432\n    // Controller:  orders.Shipments\n    // Action:      update\n    .put(\n        name="shipment",\n        pattern="shipments/[key]",\n        to="shipments##update",\n        package="orders"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="subscribers", nested=true)\n        // Route name:  launchSubscribers\n        // Example URL: /subscribers/3209/launch\n        // Controller:  Subscribers\n        // Action:      launch\n        .put(name="launch", to="subscribers##update", on="collection")\n\n        // Route name:  discontinueSubscriber\n        // Example URL: /subscribers/2251/discontinue\n        // Controller:  Subscribers\n        // Action:      discontinue\n        .put(name="discontinue", to="subscribers##discontinue", on="member")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Create a route that matches a URL requiring an HTTP PUT method. We recommend using this matcher to expose actions that update database records. This method is provided as a convenience for when you really need to support the PUT verb. Consider using the patch matcher instead of this one.\n\n","returntype":"struct","slug":"mapper.put","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"put","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic radio buttons for gender\n<cfoutput>\n<fieldset>\n    <legend>Gender</legend>\n    #radioButton(objectName="user", property="gender", tagValue="m", label="Male")#<br>\n    #radioButton(objectName="user", property="gender", tagValue="f", label="Female")#\n</fieldset>\n</cfoutput>\n\n2. Radio buttons for nested association (committee members)\n<cfoutput>\n<cfloop from="1" to="#ArrayLen(committee.members)#" index="i">\n    <div>\n        <h3>#committee.members[i].fullName#:</h3>\n        <div>\n            #radioButton(\n                objectName="committee",\n                association="members",\n                position=i,\n                property="gender",\n                tagValue="m",\n                label="Male"\n            )#<br>\n            #radioButton(\n                objectName="committee",\n                association="members",\n                position=i,\n                property="gender",\n                tagValue="f",\n                label="Female"\n            )#\n        </div>\n    </div>\n</cfloop>\n</cfoutput>\n\n3. Custom HTML wrapping and label placement\n#radioButton(\n    objectName="user",\n    property="subscription",\n    tagValue="premium",\n    label="Premium Plan",\n    prepend="<div class='radio-wrapper'>",\n    append="</div>",\n    labelPlacement="aroundRight"\n)#\n
"},"hint":"Generates an HTML radio button for a form, based on a model object’s property. It can handle simple properties as well as nested properties through associations, making it ideal for forms that work with both individual objects and collections. You can customize the radio button with additional attributes, labels, and error handling options. It automatically reflects the object’s current property value, so if the property matches the tagValue, the radio button will be marked as checked. This function helps you build dynamic forms safely and easily, with support for encoding to prevent XSS attacks, error highlighting, and custom HTML wrapping.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.radioButton","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"required":false,"hint":"The value of the radio button when selected.","name":"tagValue","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"radioButton","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic radio buttons for gender\n<cfoutput>\n<fieldset>\n    <legend>Gender</legend>\n    #radioButtonTag(name="gender", value="m", label="Male", checked=true)#<br>\n    #radioButtonTag(name="gender", value="f", label="Female")#\n</fieldset>\n</cfoutput>\n\n2. Label before radio button\n#radioButtonTag(name="subscription", value="premium", label="Premium Plan", labelPlacement="before")#\n\n3. Custom HTML wrappers\n#radioButtonTag(\n    name="newsletter",\n    value="yes",\n    label="Subscribe",\n    prepend="<div class='radio-wrapper'>",\n    append="</div>",\n    labelPlacement="aroundRight"\n)#\n
"},"hint":"Generates a standard HTML <input type=\"radio\"> element based on the supplied name and value. Unlike radioButton(), this function works directly with form tags rather than binding to a model object. It is useful for simple forms or when you need fine-grained control over the HTML attributes. You can customize the radio button with labels, label placement, HTML wrapping, and encoding to prevent XSS attacks. The generated radio button will be marked as checked if the checked argument is true.\n\n","returntype":"string","slug":"controller.radioButtonTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"required":true,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":false,"required":false,"hint":"Whether or not to check the radio button by default.","name":"checked","type":"boolean"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"radioButtonTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Testing for a specific exception\n// Assume updateUser() should throw an error if email is invalid\nerrorType = raised('model("user").updateUser({email="invalid-email"})');\nassert("errorType eq Wheels.InvalidEmailException");\n\n2. Using raised() in a test case\nfunction testInvalidPassword() {\n    var errorType = raised('model("user").login(username="jdoe", password="wrong")');\n    writeOutput("Caught error type: " & errorType);\n    // Output: Caught error type: Wheels.InvalidPassword\n}\n\n3. Catching any error\nvar errorType = raised('1 / 0'); // Division by zero\nwriteOutput(errorType);\n
"},"hint":"Used in legacy Wheels testing to catch errors or exceptions raised by a given CFML expression. It evaluates the expression and, if an error occurs, returns the type of the error. This is especially useful when writing tests to ensure that specific operations correctly trigger exceptions under invalid or unexpected conditions. By using raised(), you can assert that your code behaves safely and predictably when encountering errors.\n\n","returntype":"string","slug":"test.raised","parameters":[{"required":true,"hint":"String containing CFML expression to evaluate","name":"expression","type":"string"}],"availableIn":["test"],"name":"raised","tags":{"categoryClass":"testingfunctions","sectionClass":"testmodel","category":"Testing Functions","section":"Test Model"}},{"extended":{"hasExtended":true,"docs":"
1. Redirect to an action after saving\nif (user.save()) {\n    redirectTo(action="saveSuccessful");\n}\n\n2. Redirect to a secure checkout page with parameters\nredirectTo(\n    controller="checkout",\n    action="start",\n    params="type=express",\n    protocol="https"\n);\n\n3. Redirect to a named route and pass a route parameter\nredirectTo(route="profile", screenName="Joe");\n\n4. Redirect back to the referring page\nredirectTo(back=true);\n\n5. Redirect to an external URL\nredirectTo(url="https://example.com/welcome");\n
"},"hint":"Used to redirect the browser to another page, action, controller, route, or back to the referring page. Internally, it uses Wheels’ URLFor() function to construct the URL and the <cflocation> tag (or equivalent in your CFML engine) to perform the actual redirect. You can redirect to internal routes or controllers, pass keys and query parameters, include anchors, override protocol, host, or port, and even delay the redirect until after your action code executes. This function ensures URLs are safely encoded and properly formatted for the redirect.\n\n","returntype":"void","slug":"controller.redirectTo","parameters":[{"default":false,"required":false,"hint":"Set to `true` to redirect back to the referring page.","name":"back","type":"boolean"},{"default":false,"required":false,"hint":"See documentation for your CFML engine's implementation of `cflocation`.","name":"addToken","type":"boolean"},{"default":302,"required":false,"hint":"See documentation for your CFML engine's implementation of `cflocation`.","name":"statusCode","type":"numeric"},{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"name":"method","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: `wheels=cool&x=y`). Please note that Wheels uses the `&` and `=` characters to split the parameters and encode them properly for you. However, if you need to pass in `&` or `=` as part of the value, then you need to encode them (and only them), example: `a=cats%26dogs%3Dtrouble!&b=1`.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If `true`, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":"","required":false,"hint":"Redirect to an external URL.","name":"url","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to delay the redirection until after the rest of your action code has executed.","name":"delay","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"redirectTo","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Rerun a specific migration version\nresult = redoMigration(version="202509250915");\nwriteOutput(result); // Returns status or log of the migration rerun\n\n2. Using redoMigration in a script for testing\nif (environment() == "development") {\n    redoMigration(version="202509250920");\n}\n\n3. Rerun latest migration (if version not specified)\nresult = redoMigration();\nwriteOutput(result);\n
"},"hint":"Allows you to rerun a specific database migration version. This can be useful for testing migrations, correcting issues in a migration, or resetting a schema change during development. While it can be called directly from your application code, it is generally recommended to use this function via the CommandBox CLI or the Wheels GUI migration interface, as these provide safer execution and logging.\n\n","returntype":"string","slug":"migrator.redoMigration","parameters":[{"default":"","required":false,"hint":"The Database schema version to rerun","name":"version","type":"string"}],"availableIn":["migrator"],"name":"redoMigration","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic reference column\nt.references("userId");\n\n2. Multiple references with nulls allowed\nt.references(referenceNames="userId,orderId", allowNull=true);\n\n3. Reference with default value\nt.references(referenceNames="statusId", default=1);\n\n4. Polymorphic reference (used in polymorphic associations)\nt.references(referenceNames="referenceableId", polymorphic=true);\n\n5. Custom foreign key actions\nt.references(\n    referenceNames="customerId",\n    onUpdate="CASCADE",\n    onDelete="SET NULL"\n);\n
"},"hint":"Used when defining a table schema to add reference columns that act as foreign keys, linking the table to other tables in the database. It automatically creates integer columns for the references and sets up foreign key constraints, helping maintain referential integrity. You can customize the behavior of these reference columns, including whether they allow nulls, default values, or support polymorphic associations. You can also define actions for ON UPDATE and ON DELETE events.\n\n","returntype":"any","slug":"tabledefinition.references","parameters":[{"required":true,"name":"referenceNames","type":"string"},{"required":false,"name":"default","type":"string"},{"default":"false","required":false,"name":"allowNull","type":"boolean"},{"default":"false","required":false,"name":"polymorphic","type":"boolean"},{"default":"true","required":false,"name":"foreignKey","type":"boolean"},{"default":"","required":false,"name":"onUpdate","type":"string"},{"default":"","required":false,"name":"onDelete","type":"string"}],"availableIn":["tabledefinition"],"name":"references","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get an object, call a method on it that could potentially change values, and then reload the values from the database\nemployee = model("employee").findByKey(params.key);\nemployee.someCallThatChangesValuesInTheDatabase();\nemployee.reload();
"},"hint":"Refreshes the property values of a model object from the database. This is useful when an object’s values might have changed in the database due to other operations or external processes. By calling reload(), you ensure that your object reflects the current state of the corresponding database record.\n\n","returntype":"void","slug":"model.reload","parameters":[],"availableIn":["model"],"name":"reload","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Remove a single column from a table\nremoveColumn(table="users", columnName="middleName");\n\n2. Remove a foreign key reference column\nremoveColumn(table="orders", referenceName="customerId");\n\n3. Remove multiple columns in separate calls\nremoveColumn(table="products", columnName="oldPrice");\nremoveColumn(table="products", columnName="discountRate");\n
"},"hint":"Used to delete a column from a database table within a migration CFC. This is useful when you need to remove obsolete or incorrectly added columns during schema evolution. Optionally, you can also remove a reference column by specifying its referenceName. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.removeColumn","parameters":[{"required":true,"hint":"The table containing the column to remove","name":"table","type":"string"},{"default":"","required":false,"hint":"The column name to remove","name":"columnName","type":"string"},{"default":"","required":false,"hint":"optional reference name","name":"referenceName","type":"string"}],"availableIn":["migration"],"name":"removeColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Remove an index from the members table\nremoveIndex(table="members", indexName="members_username");\n\n2. Remove an index from the orders table\nremoveIndex(table="orders", indexName="orders_createdAt_idx");\n\n3. Remove multiple indexes in separate calls\nremoveIndex(table="products", indexName="products_name_idx");\nremoveIndex(table="products", indexName="products_category_idx");\n
"},"hint":"Used to delete an index from a database table within a migration CFC. Indexes are typically added to improve query performance, but there are scenarios where an index becomes unnecessary or needs to be replaced. Using removeIndex() allows you to safely remove an index while maintaining database integrity. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.removeIndex","parameters":[{"required":true,"hint":"The table name to perform the index operation on","name":"table","type":"string"},{"required":true,"hint":"the name of the index to remove","name":"indexName","type":"string"}],"availableIn":["migration"],"name":"removeIndex","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Remove a specific record by ID\nremoveRecord(table="users", where="id = 42");\n\n2. Remove multiple records matching a condition\nremoveRecord(table="orders", where="status = 'cancelled'");\n\n3. Remove all records from a table (use with caution)\nremoveRecord(table="temporary_data", where="1=1");\n
"},"hint":"Used to delete specific records from a database table within a migration CFC. This is useful when you need to clean up obsolete data, remove test data, or correct records as part of a schema migration. You can optionally provide a where clause to target specific rows. If no where clause is provided, the behavior depends on the database; usually, no records are removed unless explicitly specified. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.removeRecord","parameters":[{"required":true,"hint":"The table name to remove the record from","name":"table","type":"string"},{"default":"","required":false,"hint":"The where clause, i.e id = 123","name":"where","type":"string"}],"availableIn":["migration"],"name":"removeRecord","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Rename a column in the users table\nrenameColumn(table="users", columnName="username", newColumnName="user_name");\n\n2. Rename a column in the orders table\nrenameColumn(table="orders", columnName="createdAt", newColumnName="order_created_at");\n\n3. Rename multiple columns in separate migration calls\nrenameColumn(table="products", columnName="oldPrice", newColumnName="price_old");\nrenameColumn(table="products", columnName="discountRate", newColumnName="discount_percent");\n
"},"hint":"Used to change the name of an existing column in a database table within a migration CFC. This is useful when you need to standardize column names, correct naming mistakes, or improve clarity in your database schema. Renaming a column preserves the existing data and column type while updating the schema. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.renameColumn","parameters":[{"required":true,"hint":"The table containing the column to rename","name":"table","type":"string"},{"required":true,"hint":"The column name to rename","name":"columnName","type":"string"},{"required":true,"hint":"The new column name","name":"newColumnName","type":"string"}],"availableIn":["migration"],"name":"renameColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Rename the users table\nrenameTable(oldName="users", newName="app_users");\n\n2. Rename the orders table\nrenameTable(oldName="orders", newName="customer_orders");\n\n3. Rename multiple tables in separate migration calls\nrenameTable(oldName="products_old", newName="products");\nrenameTable(oldName="temp_data", newName="archived_data");\n
"},"hint":"Used to change the name of an existing database table within a migration CFC. This is helpful when you want to standardize table names, correct naming mistakes, or improve clarity in your database schema. This operation preserves all the existing data, indexes, and constraints in the table while updating its name. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.renameTable","parameters":[{"required":true,"hint":"Name the old table","name":"oldName","type":"string"},{"required":true,"hint":"New name for the table","name":"newName","type":"string"}],"availableIn":["migration"],"name":"renameTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Render an empty page with default status (200 OK)\nrenderNothing();\n\n2. Render nothing with a 204 No Content status\nrenderNothing(status="204");\n\n3. Use renderNothing in an API endpoint after deleting a resource\nfunction deleteResource() {\n    resource = model("resource").findByKey(params.id);\n    resource.delete();\n    renderNothing(status="204");\n}\n
"},"hint":"Instructs the controller to render an empty response when an action completes. Unlike using cfabort, which stops request processing immediately, renderNothing() ensures that any after filters associated with the action still execute. You can optionally provide an HTTP status code to indicate the type of response being returned. This is useful for APIs or endpoints that need to signal a specific status without returning a body.\n\n","returntype":"void","slug":"controller.renderNothing","parameters":[{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderNothing","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render a partial in the current controller's view folder\nrenderPartial("comment");\n\n2. Render a partial from the shared folder\nrenderPartial("/shared/comment");\n\n3. Render a partial without a layout\nrenderPartial(partial="/shared/comment", layout=false);\n\n4. Render a partial and return it as a string\ncommentHtml = renderPartial(partial="comment", returnAs="string");\n\n5. Render a partial with caching for 15 minutes\nrenderPartial(partial="comment", cache=15);\n\n6. Render a partial with a custom HTTP status code\nrenderPartial(partial="comment", status="202");\n
"},"hint":"Instructs the controller to render a partial view when an action completes. Partials are reusable view fragments, typically prefixed with an underscore (e.g., _comment.cfm). This function allows you to render these fragments either directly to the client or capture them as a string for further processing. You can control caching, layouts, HTTP status codes, and data-loading behavior, making it flexible for both full-page updates and AJAX responses.\n\n","returntype":"any","slug":"controller.renderPartial","parameters":[{"required":true,"hint":"The name of the partial file to be used. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Do not include the partial filename's underscore and file extension.","name":"partial","type":"string"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"string"},{"default":"","required":false,"hint":"Set to `string` to return the result instead of automatically sending it to the client.","name":"returnAs","type":"string"},{"default":true,"required":false,"hint":"Name of a controller function to load data from.","name":"dataFunction","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderPartial","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render a simple message\nrenderText("Done!");\n\n2. Render serialized product data as JSON\nproducts = model("product").findAll();\nrenderText(SerializeJson(products));\n\n3. Render a message with a custom HTTP status code\nrenderText(text="Unauthorized access", status=401);\n\n4. Use in an API endpoint\nfunction checkStatus() {\n    if (someCondition()) {\n        renderText(text="OK", status=200);\n    } else {\n        renderText(text="Error", status=500);\n    }\n}\n
"},"hint":"Instructs the controller to output plain text as the response when an action completes. Unlike rendering a view or partial, this sends the specified text directly to the client. This is especially useful for APIs, AJAX responses, or simple status messages. You can also provide an HTTP status code to control the response status.\n\n","returntype":"void","slug":"controller.renderText","parameters":[{"default":"","required":false,"hint":"The text to render.","name":"text","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"any"}],"availableIn":["controller"],"name":"renderText","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render a view page for a different action within the same controller.\nrenderView(action="edit");\n\n2. Render a view page for a different action within a different controller.\nrenderView(controller="blog", action="new");\n\n3. Another way to render the blog/new template from within a different controller.\nrenderView(template="/blog/new");\n\n4. Render the view page for the current action but without a layout and cache it for 60 minutes.\nrenderView(layout=false, cache=60);\n\n5. Load a layout from a different folder within `views`.\nrenderView(layout="/layouts/blog");\n\n6. Don't render the view immediately but rather return and store in a variable for further processing.\nmyView = renderView(returnAs="string");\n
"},"hint":"Instructs the controller which view template and layout to render when it's finished processing the action.\nNote that when passing values for controller and / or action, this function does not execute the actual action but rather just loads the corresponding view template.\n\n","returntype":"any","slug":"controller.renderView","parameters":[{"default":"[runtime expression]","required":false,"hint":"Controller to include the view page for.","name":"controller","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Action to include the view page for.","name":"action","type":"string"},{"default":"","required":false,"hint":"A specific template to render. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder.","name":"template","type":"string"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"any"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"Set to `string` to return the result instead of automatically sending it to the client.","name":"returnAs","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to hide the debug information at the end of the output. This is useful, for example, when you're testing XML output in an environment where the global setting for `showDebugInformation` is `true`.","name":"hideDebugInformation","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderView","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render all products in the requested format (json, xml, etc.)\nproducts = model("product").findAll();\nrenderWith(products);\n\n2. Render a JSON error message with a 403 status code\nmsg = {\n    "status" : "Error",\n    "message": "Not Authenticated"\n};\nrenderWith(data=msg, status=403);\n\n3. Render with a custom layout\nproducts = model("product").findAll();\nrenderWith(data=products, layout="/layouts/api");\n\n4. Render a view template from a different controller\ndata = model("order").findAll();\nrenderWith(data=data, controller="orders", action="list");\n\n5. Capture the output as a string instead of sending it to the client\noutput = renderWith(data=products, returnAs="string");\n
"},"hint":"Instructs the controller to render the given data in the format requested by the client. If the requested format is json or xml, Wheels automatically converts the data into the appropriate format. For other formats—or to override automatic formatting—you can create a view template matching the requested format, such as nameofaction.json.cfm, nameofaction.xml.cfm, or nameofaction.pdf.cfm. This function is especially useful in APIs, AJAX endpoints, or situations where you need to respond dynamically in multiple formats based on client preferences. You can also control caching, layout, HTTP status codes, and whether to return the result as a string for further processing.\n\n","returntype":"any","slug":"controller.renderWith","parameters":[{"required":true,"hint":"Data to format and render.","name":"data","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Controller to include the view page for.","name":"controller","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Action to include the view page for.","name":"action","type":"string"},{"default":"","required":false,"hint":"A specific template to render. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder.","name":"template","type":"string"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"any"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"Set to `string` to return the result instead of automatically sending it to the client.","name":"returnAs","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to hide the debug information at the end of the output. This is useful, for example, when you're testing XML output in an environment where the global setting for `showDebugInformation` is `true`.","name":"hideDebugInformation","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderWith","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
// alternating row colors and shrinking emphasis\n<cfoutput query="employees" group="departmentId">\n\t<div class="#cycle(values="even,odd", name="row")#">\n\t\t<ul>\n\t\t\t<cfoutput>\n\t\t\t\trank = cycle(values="president,vice-president,director,manager,specialist,intern", name="position")>\n\t\t\t\t<li class="#rank#">#categories.categoryName#</li>\n\t\t\t\tresetCycle("emphasis")>\n\t\t\t</cfoutput>\n\t\t</ul>\n\t</div>\n</cfoutput>
"},"hint":"Rsets a named cycle, allowing it to start from the first value the next time it is called. In Wheels, cycle() is often used to alternate values in a repeated pattern, such as CSS classes for table rows, positions, or emphasis levels. By calling resetCycle(), you ensure that the cycle begins again from its initial value, which is useful when looping through nested structures or when a new grouping starts.\n\n","returntype":"void","slug":"controller.resetCycle","parameters":[{"default":"default","required":false,"hint":"The name of the cycle to reset.","name":"name","type":"string"}],"availableIn":["controller"],"name":"resetCycle","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // With default arguments\n    .resource("checkout")\n\n    // Point auth URL to controller at `controllers/sessions/Auth.cfc`\n    .resource(name="auth", controller="sessions.auth")\n\n    // Limited list of routes generated by `only` argument.\n    .resource(name="profile", only="show,edit,update")\n\n    // Limited list of routes generated by `except` argument.\n    .resource(name="cart", except="new,create,edit,delete")\n\n    // Nested resource\n    .resource(name="preferences", only="index", nested=true)\n      .get(name="editPassword", to="passwords##edit")\n      .patch(name="password", to="passwords##update")\n\n      .resources("foods")\n    .end()\n\n    // Overridden `path`\n    .resource(name="blogPostOptions", path="blog-post/options")\n.end();\n\n</cfscript>
"},"hint":"Create a group of routes that exposes actions for manipulating a singular resource. A singular resource exposes URL patterns for the entire CRUD lifecycle of a single entity (show, new, create, edit, update, and delete) without exposing a primary key in the URL. Usually this type of resource represents a singleton entity tied to the session, application, or another resource (perhaps nested within another resource). If you need to generate routes for manipulating a collection of resources with a primary key in the URL, see the resources mapper method.\n\n","returntype":"struct","slug":"mapper.resource","parameters":[{"required":true,"hint":"Camel-case name of resource to reference when build links and form actions. This is typically a singular word (e.g., `profile`).","name":"name","type":"string"},{"default":false,"required":false,"hint":"Whether or not additional calls will be nested within this resource.","name":"nested","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Override URL path representing this resource. Default is a dasherized version of `name` (e.g., `blogPost` generates a path of `blog-post`).","name":"path","type":"string"},{"required":false,"hint":"Override name of the controller used by resource. This defaults to a pluralized version of `name`.","name":"controller","type":"string"},{"required":false,"hint":"Override singularize() result in plural resources.","name":"singular","type":"string"},{"required":false,"hint":"Override pluralize() result in singular resource.","name":"plural","type":"string"},{"required":false,"hint":"Limits the list of RESTful routes to generate. Can include `show`, `new`, `create`, `edit`, `update`, and `delete`.","name":"only","type":"string"},{"required":false,"hint":"Excludes RESTful routes to generate, taking priority over the `only` argument. Can include `show`, `new`, `create`, `edit,` `update`, and `delete`.","name":"except","type":"string"},{"required":false,"hint":"Turn on shallow resources.","name":"shallow","type":"boolean"},{"required":false,"hint":"Shallow path prefix.","name":"shallowPath","type":"string"},{"required":false,"hint":"Shallow name prefix.","name":"shallowName","type":"string"},{"required":false,"hint":"Variable patterns to use for matching.","name":"constraints","type":"struct"},{"default":"resource","required":false,"name":"$call","type":"string"},{"default":false,"required":false,"name":"$plural","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Whether or not to add an optional `.[format]` pattern to the end of the generated routes. This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"resource","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // With default arguments\n    .resources("admins")\n\n    // Point authors URL to controller at `controllers/Users.cfc`\n    .resources(name="authors", controller="users")\n\n    // Limited list of routes generated by `only` argument.\n    .resources(name="products", only="index,show,edit,update")\n\n    // Limited list of routes generated by `except` argument.\n    .resources(name="orders", except="delete")\n\n    // Nested resources\n    .resources(name="stories", nested=true)\n      .resources("heroes")\n      .resources("villains")\n    .end()\n\n    // Overridden `path`\n    .resources(name="blogPostsOptions", path="blog-posts/options")\n.end();\n\n</cfscript>
"},"hint":"Create a group of routes that exposes actions for manipulating a collection of resources. A plural resource exposes URL patterns for the entire CRUD lifecycle (index, show, new, create, edit, update, delete), exposing a primary key in the URL for showing, editing, updating, and deleting records. If you need to generate routes for manipulating a singular resource without a primary key, see the resource mapper method.\n\n","returntype":"struct","slug":"mapper.resources","parameters":[{"required":true,"hint":"Camel-case name of resource to reference when build links and form actions. This is typically a plural word (e.g., `posts`).","name":"name","type":"string"},{"default":false,"required":false,"hint":"Whether or not additional calls will be nested within this resource.","name":"nested","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Override URL path representing this resource. Default is a dasherized version of `name` (e.g., `blogPosts` generates a path of `blog-posts`).","name":"path","type":"string"},{"required":false,"hint":"Override name of the controller used by resource. This defaults to the value provided for `name`.","name":"controller","type":"string"},{"required":false,"hint":"Override singularize() result in plural resources.","name":"singular","type":"string"},{"required":false,"hint":"Override pluralize() result in singular resource.","name":"plural","type":"string"},{"required":false,"hint":"Limits the list of RESTful routes to generate. Can include `index`, `show`, `new`, `create`, `edit`, `update`, and `delete`.","name":"only","type":"string"},{"required":false,"hint":"Excludes RESTful routes to generate, taking priority over the `only` argument. Can include `index`, `show`, `new`, `create`, `edit`, `update`, and `delete`.","name":"except","type":"string"},{"required":false,"hint":"Turn on shallow resources.","name":"shallow","type":"boolean"},{"required":false,"hint":"Shallow path prefix.","name":"shallowPath","type":"string"},{"required":false,"hint":"Shallow name prefix.","name":"shallowName","type":"string"},{"required":false,"hint":"Variable patterns to use for matching.","name":"constraints","type":"struct"},{"default":"[runtime expression]","required":false,"hint":"Whether or not to add an optional `.[format]` pattern to the end of the generated routes. This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"resources","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Render a view for the current action\nrenderView(action=\"show\");\n\n// Capture the response content\nwheelsResponse = response();\n\n// Log or inspect the response\nwriteDump(wheelsResponse);
"},"hint":"Returns the content that Wheels is preparing to send back to the client for the current request. This can include the output generated by renderView(), renderPartial(), renderText(), or any other rendering function that has been called during the request lifecycle. Essentially, response() lets you inspect or manipulate the final output before it is sent to the client, which can be particularly useful in testing, debugging, or middleware-style functions.\n\n","returntype":"string","slug":"controller.response","parameters":[],"availableIn":["controller"],"name":"response","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Application Home Page\nMap the root of the application (/) to a controller action:\n\n<cfscript>\nmapper()\n    .root(to="dashboards##show")\n.end();\n</cfscript>\n\n2. Root of a Namespaced Section (API)\nMap /api to an API controller:\n\n<cfscript>\nmapper()\n    .namespace("api")\n        .root(controller="apis", action="index")\n    .end();\n</cfscript>\n\n3. Root with Optional Format\nEnable clients to request JSON or XML directly:\n\n<cfscript>\nmapper()\n    .namespace("api")\n        .root(controller="apis", action="index", mapFormat=true)\n    .end();\n</cfscript>\n\n4. Root for Nested Resources\nUse root() inside a nested scope:\n\n<cfscript>\nmapper()\n    .namespace("admin")\n        .root(controller="dashboard", action="index")\n    .end();\n.end();\n</cfscript>\n
"},"hint":"Defines a route that matches the root of the current context. This could be the root of the entire application (like the home page) or the root of a namespaced section of your routes. It is commonly used to map a controller action to the main entry point of your application or a subsection of it. You can specify the controller and action either using the to argument (controller##action) or by passing controller and action separately. Optionally, mapFormat can be set to true to allow a format suffix like .json or .xml in the URL.\n\n","returntype":"struct","slug":"mapper.root","parameters":[{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Set to `true` to include the format (e.g. `.json`) in the route.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"root","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Save (Automatic INSERT/UPDATE)\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Alice";\nuser.lastName = "Smith";\nuser.email = "alice@example.com";\n\nif(user.save()){\n    writeOutput("User saved successfully!");\n} else {\n    writeOutput("Error saving user. Please check validations.");\n}\n</cfscript>\n\n2. Save Without Validations\n\n<cfscript>\nuser = model("user").findByKey(1);\nuser.firstName = ""; // Normally fails validation\n\n// Save without running validations\nuser.save(validate=false);\n</cfscript>\n\n3. Save Using Specific cfqueryparam Columns\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Bob";\nuser.lastName = "Jones";\nuser.email = "bob@example.com";\n\n// Only parameterize the `email` field\nuser.save(parameterize="email");\n</cfscript>\n\n4. Save Within a Transaction\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Charlie";\nuser.lastName = "Brown";\nuser.email = "charlie@example.com";\n\n// Attempt to save, but roll back instead of committing\nuser.save(transaction="rollback");\n</cfscript>\n\n5. Save and Handle Callbacks Manually\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Dana";\nuser.lastName = "White";\nuser.email = "dana@example.com";\n\n// Save without triggering beforeSave/afterSave callbacks\nuser.save(callbacks=false);\n</cfscript>\n
"},"hint":"Saves the current model object to the database, with Wheels automatically determining whether to perform an INSERT for new objects or an UPDATE for existing ones. It returns true if the object was successfully saved, and false if the object failed validation or could not be saved. By default, save() also respects callbacks, validations, and parameterization, though these behaviors can be customized through optional arguments.\n\n","returntype":"boolean","slug":"model.save","parameters":[{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"}],"availableIn":["model"],"name":"save","tags":{"categoryClass":"crudfunctions","sectionClass":"modelclass","category":"CRUD Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Set a default controller for multiple routes\n\n<cfscript>\nmapper()\n    .scope(controller="freeForAll")\n        .get(name="bananas", action="bananas")\n        .root(action="index")\n    .end()\n.end();\n</cfscript>\n\n2. Apply a package/subfolder to multiple resources\n\n<cfscript>\nmapper()\n    .scope(package="public")\n        .resource(name="search", only="show,create")\n    .end()\n.end();\n</cfscript>\n\n3. Add a common URL path prefix\n\n<cfscript>\nmapper()\n    .scope(path="phones")\n        .get(name="newest", to="phones##newest")\n        .get(name="sortOfNew", to="phones##sortOfNew")\n    .end()\n.end();\n</cfscript>\n\n4. Combine controller and path scoping\n\n<cfscript>\nmapper()\n    .scope(controller="products", path="shop")\n        .get(name="featured", action="featured")\n        .get(name="sale", action="sale")\n    .end()\n.end();\n</cfscript>\n\n5. Use constraints for route variables\n\n<cfscript>\nmapper()\n    .scope(path="users", constraints={userId="\\d+"})\n        .get(name="profile", pattern="[userId]/profile", action="show")\n    .end()\n.end();\n</cfscript>\n
"},"hint":"The scope() function in Wheels is used to define a block of routes that share common parameters such as controller, package, path, or naming prefixes. All routes defined inside a scope() block automatically inherit these parameters unless explicitly overridden, making it easier to manage related routes. This is particularly useful for grouping routes under the same controller or package, adding a common URL prefix to multiple routes, applying shallow routing to nested resources, and reducing repetition while improving the maintainability of route definitions.\n\n","returntype":"struct","slug":"mapper.scope","parameters":[{"required":false,"hint":"Name to prepend to child route names for use when building links, forms, and other URLs.","name":"name","type":"string"},{"required":false,"hint":"Path to prefix to all child routes.","name":"path","type":"string"},{"required":false,"hint":"Package namespace to append to controllers.","name":"package","type":"string"},{"required":false,"hint":"Controller to use for routes.","name":"controller","type":"string"},{"required":false,"hint":"Turn on shallow resources to eliminate routing added before this one.","name":"shallow","type":"boolean"},{"required":false,"hint":"Shallow path prefix.","name":"shallowPath","type":"string"},{"required":false,"hint":"Shallow name prefix.","name":"shallowName","type":"string"},{"required":false,"hint":"Variable patterns to use for matching.","name":"constraints","type":"struct"},{"default":"scope","required":false,"name":"$call","type":"string"}],"availableIn":["mapper"],"name":"scope","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic select for seconds (0–59)\nsecondSelectTag(name="secondsToLaunch")\n\n2. Pre-select a second based on a parameter\nsecondSelectTag(name="secondsToLaunch", selected=params.secondsToLaunch)\n\n3. Only show 15-second intervals\nsecondSelectTag(name="secondsToLaunch", selected=params.secondsToLaunch, secondStep=15)\n\n4. Include a blank option with custom text\nsecondSelectTag(name="secondsToLaunch", includeBlank="- Select Seconds -")\n\n5. Add a label around the select control\nsecondSelectTag(name="secondsToLaunch", label="Launch Second", labelPlacement="around")
"},"hint":"Generates an HTML <select> form control populated with seconds (0–59) for a minute. You can bind it to a form parameter or manually set a selected value, control the step interval, include a blank option, and customize labels and HTML attributes. This is especially useful for time selection forms, like setting the seconds for a scheduled task or timestamp input.\n\n","returntype":"string","slug":"controller.secondSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"secondSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic select bound to a model property\nauthors = model("author").findAll();\n\n<!--- View code --->\n#select(objectName="book", property="authorId", options=authors)#\n\n2. Using valueField and textField\nselect(\n    objectName="book",\n    property="authorId",\n    options=authors,\n    valueField="id",\n    textField="authorfullname"\n)\n\n3. Include blank option\nselect(\n    objectName="book",\n    property="authorId",\n    options=authors,\n    valueField="id",\n    textField="authorfullname",\n    includeBlank="- Select Author -"\n)\n\n4. Nested hasMany association\n<cfloop from="1" to="#ArrayLen(shipments.orders)#" index="i">\n    select(\n        label="Order #shipments.orders[i].orderNum#",\n        objectName="shipment",\n        association="orders",\n        position=i,\n        property="statusId",\n        options=statuses,\n        valueField="id",\n        textField="name"\n    )\n</cfloop>\n\n5. Custom label and placement\nselect(\n    objectName="book",\n    property="authorId",\n    options=authors,\n    valueField="id",\n    textField="authorfullname",\n    label="Choose Author",\n    labelPlacement="before"\n)
"},"hint":"Builds and returns an HTML <select> element bound to a model object property. It automatically handles nested associations, labels, options, and error highlighting. You can provide a list of options as a query, array of objects, or simple array, and customize labels, HTML attributes, and encoding. It is especially useful for forms where a user must select a value from a predefined list that is related to a database model.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.select","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"required":false,"hint":"A collection to populate the select form control with. Can be a query recordset or an array of objects.","name":"options","type":"any"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `textField`","name":"valueField","type":"string"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element that the end user will see. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `valueField`","name":"textField","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"select","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic selectTag with a query\ncities = model("city").findAll();\n\n<!--- View code --->\n#selectTag(name="cityId", options=cities)#\n\n2. SelectTag with valueField and textField\nselectTag(\n    name="cityId",\n    options=cities,\n    valueField="id",\n    textField="name"\n)\n\n3. Including a blank option\nselectTag(\n    name="cityId",\n    options=cities,\n    valueField="id",\n    textField="name",\n    includeBlank="- Select a City -"\n)\n\n4. Multiple selection\nselectTag(\n    name="cityIds",\n    options=cities,\n    valueField="id",\n    textField="name",\n    multiple=true\n)\n\n5. Custom label and HTML wrapping\nselectTag(\n    name="cityId",\n    options=cities,\n    valueField="id",\n    textField="name",\n    label="Choose a City",\n    labelPlacement="before",\n    prepend="<div class='input-group'>",\n    append="</div>"\n)
"},"hint":"Builds an HTML <select> element using a name and a set of options. Unlike select(), it does not require a model object and is not bound to a property. It is useful for standalone select controls or when you want full manual control over the field.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.selectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"required":true,"hint":"A collection to populate the select form control with. Can be a query recordset or an array of objects.","name":"options","type":"any"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"Whether to allow multiple selection of options in the select form control.","name":"multiple","type":"boolean"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `textField`","name":"valueField","type":"string"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element that the end user will see. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `valueField`","name":"textField","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"selectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic email to a new user\nnewMember = model("member").findByKey(params.member.id);\n\nsendEmail(\n    to=newMember.email,\n    template="welcomeEmail",\n    subject="Thank You for Joining!",\n    recipientName=newMember.name,\n    startDate=newMember.startDate\n);\n\n2. Multipart email (HTML + text)\nsendEmail(\n    to="user@example.com",\n    template="welcomeEmailText, welcomeEmailHTML",\n    subject="Welcome!",\n    detectMultipart=true\n);\n\n3. Email with a layout\nsendEmail(\n    to="user@example.com",\n    template="newsletter",\n    layout="emailLayout",\n    subject="Monthly Newsletter",\n    userName="Salman"\n);\n\n4. Email with attachments\nsendEmail(\n    to="user@example.com",\n    template="reportEmail",\n    subject="Your Monthly Report",\n    file="report.pdf, summary.xlsx"\n);\n\n5. Write email to a file without sending\nsendEmail(\n    to="user@example.com",\n    template="testEmail",\n    subject="Testing Email",\n    writeToFile="#expandPath('./tmp/testEmail.eml')#",\n    deliver=false\n);
"},"hint":"Sends an email using a template and an optional layout to wrap it in.\nBesides the Wheels-specific arguments documented here, you can also pass in any argument that is accepted by the cfmail tag as well as your own arguments to be used by the view.\n\n","returntype":"any","slug":"controller.sendEmail","parameters":[{"default":"","required":true,"hint":"The path to the email template or two paths if you want to send a multipart email. if the `detectMultipart` argument is `false`, the template for the text version should be the first one in the list. This argument is also aliased as `templates`.","name":"template","type":"string"},{"default":"","required":true,"hint":"Email address to send from.","name":"from","type":"string"},{"default":"","required":true,"hint":"List of email addresses to send the email to.","name":"to","type":"string"},{"default":"","required":true,"hint":"The subject line of the email.","name":"subject","type":"string"},{"default":false,"required":false,"hint":"Layout(s) to wrap the email template in. This argument is also aliased as `layouts`.","name":"layout","type":"any"},{"default":"","required":false,"hint":"A list of the names of the files to attach to the email. This will reference files stored in the `files` folder (or a path relative to it). This argument is also aliased as `files`.","name":"file","type":"string"},{"default":true,"required":false,"hint":"When set to `true` and multiple values are provided for the `template` argument, Wheels will detect which of the templates is text and which one is HTML (by counting the `<` characters).","name":"detectMultipart","type":"boolean"},{"default":true,"required":false,"hint":"When set to `false`, the email will not be sent.","name":"deliver","type":"boolean"},{"default":"","required":false,"hint":"The file to which the email contents will be written","name":"writeToFile","type":"string"}],"availableIn":["controller"],"name":"sendEmail","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Send a file for download from the files folder\nsendFile(file="wheels_tutorial_20081028_J657D6HX.pdf");\n\n2. Rename the file for the client\nsendFile(\n    file="wheels_tutorial_20081028_J657D6HX.pdf",\n    name="Tutorial.pdf"\n);\n\n3. Send a file located outside the web root\nsendFile(\n    file="../../tutorials/wheels_tutorial_20081028_J657D6HX.pdf"\n);\n\n4. Inline display instead of download\nsendFile(\n    file="brochure.pdf",\n    disposition="inline",\n    type="application/pdf"\n);\n\n5. Delete file after sending\nsendFile(\n    file="temporary_report.xlsx",\n    deleteFile=true\n);
"},"hint":"Sends a file to the client. By default, it serves files from the public/files folder in your project or a path relative to it. You can control how the file is presented to the user (download dialog vs inline display), set the content type, rename it for the client, or even delete it from the server after delivery.\n\n","returntype":"any","slug":"controller.sendFile","parameters":[{"required":true,"hint":"The file to send to the user.","name":"file","type":"string"},{"default":"","required":false,"hint":"The file name to show in the browser download dialog box.","name":"name","type":"string"},{"default":"","required":false,"hint":"The HTTP content type to deliver the file as.","name":"type","type":"string"},{"default":"attachment","required":false,"hint":"Set to `inline` to have the browser handle the opening of the file (possibly inline in the browser) or set to `attachment` to force a download dialog box.","name":"disposition","type":"string"},{"default":"","required":false,"hint":"Directory outside of the web root where the file exists. Must be a full path.","name":"directory","type":"string"},{"default":false,"required":false,"hint":"Pass in `true` to delete the file on the server after sending it.","name":"deleteFile","type":"boolean"},{"default":true,"required":false,"name":"deliver","type":"boolean"}],"availableIn":["controller"],"name":"sendFile","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Set the `URLRewriting` setting to `Partial`.\nset(URLRewriting="Partial");\n\n2. Set default values for the arguments in the `buttonTo` view helper. This works for the majority of Wheels functions/arguments.\nset(functionName="buttonTo", onlyPath=true, host="", protocol="", port=0, text="", confirm="", image="", disable="");\n\n3. Set the default values for a form helper to get the form marked up to your preferences.\nset(functionName="textField", labelPlacement="before", prependToLabel="<div>", append="</div>", appendToLabel="<br>"):\n
"},"hint":"Used to configure global settings or set default argument values for Wheels functions. It can be applied to core functions, helpers, and even migrations. This allows you to define a standard behavior across your application without repeating arguments every time a function is called.\n\n","returntype":"void","slug":"controller.set","parameters":[],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"set","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"configuration","category":"Miscellaneous Functions","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic filter chain\n// Set filter chain directly\nsetFilterChain([\n    {through="restrictAccess"}, // runs for all actions by default\n    {through="isLoggedIn, checkIPAddress", except="home, login"}, // exclude certain actions\n    {type="after", through="logConversion", only="thankYou"} // after filter for specific action\n]);\n\n//First filter: restrictAccess runs before all actions.\n//Second filter: isLoggedIn and checkIPAddress run before all actions except home and login.\n//Third filter: logConversion runs after the thankYou action only.\n\n2. Using only and except with different filter types\nsetFilterChain([\n    {through="authenticateUser", only="edit, update, delete"}, // only for sensitive actions\n    {through="trackActivity", except="index, show"},           // for most actions except viewing\n    {type="after", through="sendAnalytics"}                   // after all actions\n]);\n\n//Demonstrates selective filtering with only and except.\n//Can combine before (default) and after filters in the same chain.\n\n3. Multiple filters in one chain struct\nsetFilterChain([\n    {through="validateSession, checkPermissions", only="admin, settings"},\n    {through="logRequest"},\n    {type="after", through="cleanupTempFiles"}\n]);\n\n//Multiple filters can run together (validateSession and checkPermissions).\n//Mix of before and after filters ensures proper order and execution context.
"},"hint":"Provides a low-level way to define the complete filter chain for a controller. This lets you explicitly specify the sequence of filters, their scope, and the actions they apply to, all in a single configuration. Filters are functions that run before, after, or around actions to handle tasks such as authentication, logging, or IP restrictions.\n\n","returntype":"void","slug":"controller.setFilterChain","parameters":[{"required":true,"hint":"An array of structs, each of which represent an `argumentCollection` that get passed to the `filters` function. This should represent the entire filter chain that you want to use for this controller.","name":"chain","type":"array"}],"availableIn":["controller"],"name":"setFilterChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Set the flash to cookie for the current controller only.\nsetFlashStorage("cookie");\n\n2. Set the flash to session for the current controller and application\nsetFlashStorage("session", true);
"},"hint":"Dynamically sets the storage mechanism for flash messages during the current request lifecycle. Flash messages are temporary messages (e.g., success or error notifications) that persist across requests.\n\n","returntype":"void","slug":"controller.setFlashStorage","parameters":[{"default":"session","required":false,"hint":"Specifies the storage mechanism for flash data. Available options: session or cookie.","name":"storage","type":"string"},{"default":false,"required":false,"hint":"If set to true, updates both application-level and controller-level flashStorage; otherwise, only the controller-level flashStorage is updated.","name":"setGlobally","type":"boolean"}],"availableIn":["controller"],"name":"setFlashStorage","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
/* Note that there are two ways to do pagination yourself using a custom query.\n\t1) Do a query that grabs everything that matches and then use\n\tthe `cfouput` or `cfloop` tag to page through the results.\n\t2) Use your database to make 2 queries. The first query\n\tbasically does a count of the total number of records that match\n\tthe criteria and the second query actually selects the page of\n\trecords for retrieval.\n\tIn the example below, we will show how to write a custom query\n\tusing both of these methods. Note that the syntax where your\n\tdatabase performs the pagination will differ depending on the\n\tdatabase engine you are using. Plese consult your database\n\tengine's documentation for the correct syntax.\n\tAlso note that the view code will differ depending on the method\n\tused.\n*/\n\n//=================== First method: Handle the pagination through your CFML engine\n\n// Model code: In your model (ie. User.cfc), create a custom method for your custom query\nfunction myCustomQuery(required numeric page, numeric perPage=25){\n\tlocal.customQuery=QueryExecute("SELECT * FROM users", [], { datasource=get('dataSourceName') });\n\tsetPagination(\n\t\ttotalRecords=local.customQuery.RecordCount,\n\t\tcurrentPage=arguments.page,\n\t\tperPage=arguments.perPage,\n\t\thandle="myCustomQueryHandle");\n\treturn local.customQuery;\n}\n\n// Controller code\nfunction list(){\n\tparam name="params.page" default="1;\n\tparam name="params.perPage" default="25";\n\tallUsers = model("user").myCustomQuery( page=params.page, perPage=params.perPage);\n\n\t// Because we're going to let `cfoutput`/`cfloop` handle the pagination,\n\t// we're going to need to get some addition information about the pagination.\n\tpaginationData = pagination("myCustomQueryHandle")\n}\n\n<!--- View code (using `cfloop`): Use the information from `paginationData` to page through the records --->\n<ul>\n\t<cfloop query="allUsers" startrow="#paginationData.startrow#" endrow="#paginationData.endrow#" >\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfloop>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n<!--- View code (using `cfoutput`) Use the information from `paginationData` to page through the records--->\n<ul>\n\t<cfoutput query="allUsers" startrow="#paginationData.startrow#" maxrows="#paginationData.maxrows#" >\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfoutput>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n//=================== Second method: Handle the pagination through the database\n\n// Model code: In your model (ie. `User.cfc`), create a custom method for your custom query\n\nfunction myCustomQuery(required numeric page, numeric perPage=25){\n\tlocal.customQueryCount=QueryExecute("SELECT COUNT(*) AS theCount FROM users",\n\t\t\t\t\t\t\t\t\t\t[], { datasource=get('dataSourceName') });\n\tlocal.customQuery=QueryExecute("SELECT * FROM users LIMIT ? OFFSET ?",\n\t\t\t\t\t\t\t\t\t[arguments.page, arguments.perPage],\n\t\t\t\t\t\t\t\t\t{ datasource=get('dataSourceName') });\n\n\t//Notice the we use the value from the first query for `totalRecords`\n\tsetPagination(\n\t\ttotalRecords=local.customQueryCount.theCount,\n\t\tcurrentPage=arguments.page,\n\t\tperPage=arguments.perPage,\n\t\thandle="myCustomQueryHandle" );\n\n\t// We return the second query\n\treturn local.customQuery;\n}\n\n// Controller code\nfunction list(){\n\tparam name="params.page" default="1;\n\tparam name="params.perPage" default="25";\n\tallUsers = model("user").myCustomQuery( page=params.page, perPage=params.perPage);\n}\n\n<!--- View code (using `cfloop`)--->\n<ul>\n\t<cfloop query="allUsers">\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfloop>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n<!--- View code (using `cfoutput`)--->\n<ul>\n\t<cfoutput query="allUsers">\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfoutput>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n
"},"hint":"Aallows you to define a pagination handle for a custom query so that you can easily generate paginated links and manage page offsets in your views. It’s useful when you want manual or database-driven pagination instead of relying on built-in model queries. This works in combination with the pagination() function to retrieve pagination metadata (like startRow, endRow, maxRows) and paginationLinks() to render links in your view.\n\n","returntype":"void","slug":"controller.setPagination","parameters":[{"required":true,"hint":"Total count of records that should be represented by the paginated links.","name":"totalRecords","type":"numeric"},{"default":1,"required":false,"hint":"Page number that should be represented by the data being fetched and the paginated links.","name":"currentPage","type":"numeric"},{"default":25,"required":false,"hint":"Number of records that should be represented on each page of data.","name":"perPage","type":"numeric"},{"default":"query","required":false,"hint":"Name of handle to reference in `paginationLinks`.","name":"handle","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"setPagination","tags":{"categoryClass":"paginationfunctions","sectionClass":"controller","category":"Pagination Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Single primary key\ncomponent extends="Model" {\n    function config() {\n        // The primary key for this table is `userID`\n        setPrimaryKey("userID");\n    }\n}\n\n2. Composite primary key\ncomponent extends="Model" {\n    function config() {\n        // The combination of `orderID` and `productID` uniquely identifies a record\n        setPrimaryKey("orderID,productID");\n    }\n}\n\n3. Using the alias setPrimaryKeys()\ncomponent extends="Model" {\n    function config() {\n        // Alias works the same as `setPrimaryKey()`\n        setPrimaryKeys("customerID");\n    }\n}
"},"hint":"The setPrimaryKey() function allows you to define which property (or properties) of a model represent the primary key in the database. This is crucial for Wheels to correctly handle CRUD operations, updates, and record lookups. For single-column primary keys, pass the property name as a string. For composite primary keys (multiple columns together form the key), pass a comma-separated list of property names. Alias: setPrimaryKeys()\n\n","returntype":"void","slug":"model.setPrimaryKey","parameters":[{"required":true,"hint":"Property (or list of properties) to set as the primary key.","name":"property","type":"string"}],"availableIn":["model"],"name":"setPrimaryKey","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// In `models/Subscription.cfc`, define the primary key as composite of the columns `customerId` and `publicationId`.\nfunction config(){\n\tsetPrimaryKeys("customerId,publicationId");\n}
"},"hint":"Alias for setPrimaryKey().\nUse this for better readability when you're setting multiple properties as the primary key.\n\n","returntype":"void","slug":"model.setPrimaryKeys","parameters":[{"required":true,"hint":"Property (or list of properties) to set as the primary key.","name":"property","type":"string"}],"availableIn":["model"],"name":"setPrimaryKeys","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Using a struct (common scenario with form submission)\n// Controller code: create new user\nuser = model("user").new();\n\n// Set properties from a submitted form\nuser.setProperties(params.user);\n\n// Save the updated user\nuser.save();\n\n2. Using named arguments\nuser = model("user").new();\n\n// Set properties directly using named arguments\nuser.setProperties(\n    firstName="John",\n    lastName="Doe",\n    email="john.doe@example.com"\n);\n\n// Save changes\nuser.save();\n\n3. Using with validations\nuser = model("user").new();\n\n// Set multiple properties, skipping one intentionally\nuser.setProperties({\n    firstName = "Jane",\n    lastName = "Smith"\n});\n\n// Only save if validations pass\nif(user.save()){\n    writeOutput("User updated successfully!");\n} else {\n    writeDump(user.errors);\n}
"},"hint":"Allows you to set multiple properties of a model object at once. It is useful when you want to update a model with a structure (struct) of key/value pairs instead of assigning each property individually. The keys of the struct should match the property names of the model. You can also pass named arguments directly instead of a struct.\n\n","returntype":"void","slug":"model.setProperties","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"}],"availableIn":["model"],"name":"setProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Sending plain text\nfunction myAction() {\n    setResponse("This is a custom response sent directly to the client.");\n}\n\n2. Sending JSON content\nfunction getUserData() {\n    user = model("user").findByKey(1);\n    \n    // Convert the user object to JSON\n    jsonData = serializeJson(user);\n    \n    // Set the JSON response\n    setResponse(jsonData);\n}\ncfheader(name="Content-Type", value="application/json");\n\n3. Sending HTML content\nfunction showCustomHtml() {\n    htmlContent = "<h1>Welcome!</h1><p>This is a custom HTML response.</p>";\n    setResponse(htmlContent);\n}
"},"hint":"Allows you to manually set the content that Wheels will send back to the client for a given request. Unlike renderView() or renderText(), which automatically generate output from templates or data, setResponse() gives you full control over the response content.\n\n","returntype":"void","slug":"controller.setResponse","parameters":[{"required":true,"hint":"The content to send to the client.","name":"content","type":"string"}],"availableIn":["controller"],"name":"setResponse","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Basic prefix\n// In app/models/User.cfc\nfunction config(){\n    // All queries will now target 'tblUsers' instead of 'users'\n    setTableNamePrefix("tbl");\n}\n\n2. Using a custom prefix for multiple models\n// app/models/Product.cfc\nfunction config(){\n    setTableNamePrefix("tbl");\n}\n\n// app/models/Order.cfc\nfunction config(){\n    setTableNamePrefix("tbl");\n}
"},"hint":"Allows you to add a prefix to the table name used by a model when performing SQL queries. This is useful if your database uses a consistent naming convention, such as tblUsers instead of Users. By default, Wheels infers the table name from the model name (e.g., User -> users). Using a prefix ensures that all queries automatically reference the correctly prefixed table.\n\n","returntype":"void","slug":"model.setTableNamePrefix","parameters":[{"required":true,"hint":"A prefix to prepend to the table name.","name":"prefix","type":"string"}],"availableIn":["model"],"name":"setTableNamePrefix","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic setup for a test suite\ncomponent extends="app.tests.Test" {\n\n    function setup() {\n        // Initialize a new user object before each test\n        variables.user = model("user").new();\n    }\n\n    function test_User_Creation() {\n        variables.user.firstName = "John";\n        variables.user.lastName = "Doe";\n\n        assert("variables.user.save() eq true");\n    }\n\n    function test_User_Email_Validation() {\n        variables.user.email = "invalid-email";\n\n        assert("variables.user.valid() eq false");\n    }\n}\n\n2. Reset database table before each test\ncomponent extends="app.tests.Test" {\n\n    function setup() {\n        // Delete all records in the users table before each test\n        model("user").deleteAll();\n    }\n\n    function test_User_Insert() {\n        newUser = model("user").new(firstName="Alice", lastName="Smith");\n        assert("newUser.save() eq true");\n    }\n\n    function test_User_Count() {\n        count = model("user").count();\n        assert("count eq 0");\n    }\n}
"},"hint":"Callback used in Wheels legacy testing framework. It runs before every individual test case within a test suite. This allows you to prepare the test environment, initialize objects, or reset state before each test executes.\n\n","returntype":"any","slug":"test.setup","parameters":[],"availableIn":["test"],"name":"setup","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic verification chain\ncomponent extends="Controller" {\n\n    function init() {\n        // Set verification rules for multiple actions\n        setVerificationChain([\n            {only="handleForm", post=true},\n            {only="edit", get=true, params="userId", paramsTypes="integer"}\n        ]);\n    }\n\n    function handleForm() {\n        // Action logic here\n    }\n\n    function edit() {\n        // Action logic here\n    }\n}\n\n2. Adding custom error handling\ncomponent extends="Controller" {\n\n    function init() {\n        setVerificationChain([\n            {only="edit", get=true, params="userId", paramsTypes="integer", handler="index", error="Invalid userId"},\n            {only="delete", post=true, params="id", paramsTypes="integer", error="Missing or invalid id"}\n        ]);\n    }\n\n    function edit() {\n        /* edit logic */ \n    }\n    function delete() {\n        /* delete logic */ \n    }\n}
"},"hint":"Allows you to define the entire verification chain for a controller in a low-level, structured way. Verification chains are used to validate requests, ensuring they meet specific requirements (like HTTP method, parameters, or types) before the controller action executes. Instead of defining individual verifies() calls in each action, you can use setVerificationChain() to set all verifications at once.\n\n","returntype":"void","slug":"controller.setVerificationChain","parameters":[{"required":true,"hint":"An array of structs, each of which represent an `argumentCollection` that get passed to the `verifies` function. This should represent the entire verification chain that you want to use for this controller.","name":"chain","type":"array"}],"availableIn":["controller"],"name":"setVerificationChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Typical usage\n#simpleFormat(post.bodyText)#\n\nIf post.bodyText =\nThis is the first line.\n\nThis is the second paragraph.\n\nOutput:\n\n<p>This is the first line.</p>\n<p>This is the second paragraph.</p>\n\n2. Demonstrating line breaks\n<cfsavecontent variable="comment">\nI love this post!\n\nHere's why:\n* Short\n* Succinct\n* Awesome\n</cfsavecontent>\n\n#simpleFormat(comment)#\n\nOutput:\n\n<p>I love this post!</p>\n<p>Here's why:<br>\n* Short<br>\n* Succinct<br>\n* Awesome</p>\n\n3. Disable paragraph wrapping\n<cfsavecontent variable="bio">\nHello, I’m Salman.\nI write about ColdFusion and backend development.\n</cfsavecontent>\n\n#simpleFormat(bio, wrap=false)#\n\nOutput:\n\nHello, I’m Salman.<br>\nI write about ColdFusion and backend development.\n\n//No <p> tags, only <br> for newlines.\n\n4. Handling user input safely\nWhen you’re rendering user-submitted text in HTML attributes, simpleFormat() alone is not enough:\n\n<!-- Incorrect usage in an attribute -->\n<div title="#simpleFormat(userInput)#">...</div>\n\nInstead, combine with EncodeForHtmlAttribute():\n\n<div title="#EncodeForHtmlAttribute(simpleFormat(userInput))#">...</div>
"},"hint":"Takes plain text and converts newline and carriage return characters into HTML <br> and <p> tags for display in a browser. This is particularly useful for rendering user-submitted text (like blog posts, comments, or descriptions) in a way that respects the author’s formatting. By default, the text is wrapped in a <p> element and URL parameters are encoded for safety.\n\n","returntype":"string","slug":"controller.simpleFormat","parameters":[{"required":true,"hint":"The text to format.","name":"text","type":"string"},{"default":true,"required":false,"hint":"Set to `true` to wrap the result in a paragraph HTML element.","name":"wrap","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"simpleFormat","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Simple plural -> singular\n#singularize("languages")#\n\nOutput:\nlanguage\n\n2. Words ending in -ies\n#singularize("companies")#\n\nOutput:\ncompany\n\n3. Words ending in -es\n#singularize("boxes")#\n\nOutput:\nbox\n\n4. Irregular plural\n#singularize("children")#\n\nOutput:\nchild
"},"hint":"Converts a plural word into its singular form. It uses Wheels’ built-in inflection rules, handling common English pluralization cases as well as irregular words. This is useful when dynamically generating model names, table names, or working with resource naming conventions.\n\n","returntype":"string","slug":"controller.singularize","parameters":[{"required":true,"name":"word","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"singularize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic form for create action\n#startFormTag(action="create")#\n    #textFieldTag(name="firstName")#\n    #submitTag(value="Save")#\n#endFormTag()#\n\n2. Form with file upload\n#startFormTag(action="upload", multipart=true)#\n    #fileFieldTag(name="profilePicture")#\n    #submitTag(value="Upload")#\n#endFormTag()#\n\n3. Using a named route\n#startFormTag(route="registerUser")#\n    #textFieldTag(name="email")#\n    #passwordFieldTag(name="password")#\n    #submitTag(value="Register")#\n#endFormTag()#\n\n4. Passing keys and params\n#startFormTag(controller="posts", action="edit", key=42, params="draft=true")#\n    #textAreaTag(name="content")#\n    #submitTag(value="Update Post")#\n#endFormTag()#\n\n5. Custom attributes\n#startFormTag(action="search", id="searchForm", class="inline-form")#\n    #textFieldTag(name="q")#\n    #submitTag(value="Search")#\n#endFormTag()#
"},"hint":"Builds and returns an opening <form> tag. The form’s action URL is automatically generated following the same rules as urlFor(). You can pass standard Wheels routing arguments (controller, action, route, key, params) as well as custom HTML attributes (id, class, rel, etc.). Use this in combination with endFormTag() to wrap your form controls.\n\n","returntype":"string","slug":"controller.startFormTag","parameters":[{"default":"post","required":false,"hint":"The type of `method` to use in the `form` tag (`delete`, `get`, `patch`, `post`, and `put` are the options).","name":"method","type":"string"},{"default":false,"required":false,"hint":"Set to `true` if the form should be able to upload files.","name":"multipart","type":"boolean"},{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: wheels=cool&x=y). Please note that Wheels uses the & and = characters to split the parameters and encode them properly for you. However, if you need to pass in & or = as part of the value, then you need to encode them (and only them), example: a=cats%26dogs%3Dtrouble!&b=1.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If true, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"startFormTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple string column\nt.string("username");\n\n2. Limit the length of the string\nt.string(columnNames="email", limit=255);\n\n3. Set default values\nt.string(columnNames="status", default="active");\n\n4. Multiple columns in one call\nt.string(columnNames="firstName,lastName");\n\n5. Nullable vs non-nullable\nt.string(columnNames="configKey", allowNull=false);\nt.string(columnNames="configValue", allowNull=true);
"},"hint":"Used to add one or more string (VARCHAR) columns to a database table. It supports specifying default values, nullability, and a maximum length (limit). Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.string","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"any"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"string","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Remove links but keep text\n#stripLinks('<strong>Wheels</strong> is a framework for <a href="http://www.adobe.com/products/coldfusion">ColdFusion</a>.')#\n\nOutput:\n<strong>Wheels</strong> is a framework for ColdFusion.\n\n2. Strip links from user-submitted content\nuserComment = '<p>Check out <a href="http://spam.com">this link</a>!</p>';\n#stripLinks(userComment)#\n\nOutput:\n<p>Check out this link!</p>\n\n3. Encoding URLs (optional)\n#stripLinks('<a href="http://example.com/page?param=value&another=1">Example</a>', encode=false)#\n\nOutput:\nExample
"},"hint":"Removes all <a> tags (hyperlinks) from an HTML string while preserving the inner text. This is useful when you want to display content without clickable links but still retain the text inside them.\n\n","returntype":"string","slug":"controller.stripLinks","parameters":[{"required":true,"hint":"The HTML to remove links from.","name":"html","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"stripLinks","tags":{"categoryClass":"sanitizationfunctions","sectionClass":"viewhelpers","category":"Sanitization Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Remove all tags from a string\n#stripTags('<strong>Wheels</strong> is a framework for <a href="http://www.adobe.com/products/coldfusion">ColdFusion</a>.')#\n\nOutput:\nWheels is a framework for ColdFusion.\n\n2. Sanitize user input\nuserInput = '<script>alert("xss")</script>Normal text';\n#stripTags(userInput)#\n\nOutput:\nNormal text\n\n3. With encoding\n#stripTags('<a href="http://example.com/page?param=value&another=1">Example</a>')#\n\nOutput:\nExample
"},"hint":"Removes all HTML tags from a string, leaving only the raw text content. Use this when you need to sanitize HTML by completely removing formatting and markup.\n\n","returntype":"string","slug":"controller.stripTags","parameters":[{"required":true,"hint":"The HTML to remove tag markup from.","name":"html","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"stripTags","tags":{"categoryClass":"sanitizationfunctions","sectionClass":"viewhelpers","category":"Sanitization Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<!--- view code --->\n<head>\n    <!--- Includes `public/stylesheets/styles.css` --->\n    #styleSheetLinkTag("styles")#\n    <!--- Includes `public/stylesheets/blog.css` and `public/stylesheets/comments.css` --->\n    #styleSheetLinkTag("blog,comments")#\n    <!--- Includes printer style sheet --->\n    #styleSheetLinkTag(sources="print", media="print")#\n    <!--- Includes external style sheet --->\n    #styleSheetLinkTag("http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/cupertino/jquery-ui.css")#\n</head>\n\n<body>\n    <!--- This will still appear in the `head` --->\n    #styleSheetLinkTag(sources="tabs", head=true)#\n</body>
"},"hint":"Generates one or more <link> tags for including CSS stylesheets in your application. By default, it looks in the publicstylesheets folder of your app but can also handle external URLs or place stylesheets directly in the <head> section when needed.\n\n","returntype":"string","slug":"controller.styleSheetLinkTag","parameters":[{"default":"","required":false,"hint":"The name of one or many CSS files in the stylesheets folder, minus the `.css` extension. Pass a full URL to generate a tag for an external style sheet. Can also be called with the `source` argument.","name":"sources","type":"string"},{"default":"text/css","required":false,"hint":"The `type` attribute for the `link` tag.","name":"type","type":"string"},{"default":"all","required":false,"hint":"The `media` attribute for the `link` tag.","name":"media","type":"string"},{"required":false,"hint":"The `rel` attribute for the relation between the tag and href.","name":"rel","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to place the output in the `head` area of the HTML page instead of the default behavior (which is to place the output where the function is called from).","name":"head","type":"boolean"},{"default":",","required":false,"hint":"The delimiter to use for the list of CSS files.","name":"delim","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"styleSheetLinkTag","tags":{"categoryClass":"assetfunctions","sectionClass":"viewhelpers","category":"Asset Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Default submit button\n#startFormTag(action="save")##submitTag()##endFormTag()#\n2. Custom button label\n#submitTag(value="Register Now")#\n3. Submit button with CSS class and ID\n#submitTag(value="Update Profile", class="btn btn-primary", id="updateBtn")#\n4. Submit as an image button\n#submitTag(image="submit-icon.png", value="Submit Form")#\n5. Wrapping with prepend and append\n#submitTag(value="Send Message", prepend="<div class='form-actions'>", append="</div>")#
"},"hint":"Builds and returns a string containing a submit button form control.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.submitTag","parameters":[{"default":"Save changes","required":false,"hint":"Message to display in the button form control.","name":"value","type":"string"},{"default":"","required":false,"hint":"File name of the image file to use in the button form control.","name":"image","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"submitTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic sum\nallSalaries = model("employee").sum("salary");\n\n2. With filtering (where)\nallAustralianSalaries = model("employee").sum(\n property="salary",\n include="country",\n where="countryname='Australia'"\n);\n\n3. With ifNull safeguard\nsalarySum = model("employee").sum(\n property="salary",\n where="salary BETWEEN #params.min# AND #params.max#",\n ifNull=0\n);\n\n4. Sum with grouping\nsalariesByDept = model("employee").sum(\n property="salary",\n group="departmentId"\n);\n\n5. Distinct sum\nuniqueSalaries = model("employee").sum(\n property="salary",\n distinct=true\n);\n
"},"hint":"Calculates the total of all values for a given property (column) using SQL’s SUM() function. It’s typically used to aggregate numeric values across a set of records (e.g., summing salaries, prices, or quantities). You can add filtering with where, group results with group, or join associations using include. If no records are found, use the ifNull argument to return a safe default (commonly 0 for numeric sums).\n\n","returntype":"any","slug":"model.sum","parameters":[{"required":true,"hint":"Name of the property to get the sum for (must be a property of a numeric data type).","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":false,"required":false,"hint":"When true, SUM returns the sum of unique values only.","name":"distinct","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"sum","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic override for custom table name\n// In app/models/User.cfc\nfunction config() {\n // Tell Wheels to use the `tbl_USERS` table instead of the default `users`.\n table("tbl_USERS");\n}\n\n2. Using a table with a completely different name\n// In app/models/Order.cfc\nfunction config() {\n // Map the Order model to a table named `sales_transactions`.\n table("sales_transactions");\n}\n\n3. Disabling table mapping for a non-database model\n// In app/models/Notification.cfc\nfunction config() {\n // This model will not connect to any table.\n table(false);\n}\n\n4. Working with legacy naming conventions\n// In app/models/Product.cfc\nfunction config() {\n // The database uses uppercase with prefixes for tables.\n table("LEGACY_PRODUCTS_TABLE");\n}\n
"},"hint":"Used to tell Wheels which database table a model should connect to. Normally, Wheels automatically maps a model name to a plural table name (for example, a model named User maps to the users table). However, when your database uses custom naming conventions that do not match the Wheels defaults, you can override the mapping by explicitly specifying the table name with table(). If you want a model to not be tied to any database table at all, you can set table(false). This is useful for models that are used purely for logic, service layers, or scenarios where the model acts as a data wrapper without persistence.\n\n","returntype":"void","slug":"model.table","parameters":[{"required":true,"hint":"Name of the table to map this model to.","name":"name","type":"any"}],"availableIn":["model"],"name":"table","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Check what table the user model uses\nwhatAmIMappedTo = model("user").tableName();
"},"hint":"Returns the name of the database table that a model is mapped to. Wheels automatically determines the table name based on its naming convention, where a singular model name maps to a plural table name (for example, a model named User maps to the users table). If the table has been explicitly overridden using the table() function in the model’s config(), then tableName() will return the custom mapping instead. This function is useful when you want to programmatically check or log the database table a model is connected to, especially in projects with mixed or legacy naming conventions.\n\n","returntype":"string","slug":"model.tableName","parameters":[],"availableIn":["model"],"name":"tableName","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic cleanup after tests\nfunction teardown() {\n // Remove temporary data created during the test\n queryExecute("DELETE FROM users WHERE email LIKE 'testuser%@example.com'");\n}\n\n2. Resetting application variables\nfunction teardown() {\n // Clear session values to avoid leaking between tests\n structClear(session);\n}\n\n3. Rolling back test data with transactions\nfunction teardown() {\n // Roll back the transaction started in setup\n transaction action="rollback";\n}\n\n4. Cleaning up mock objects or stubs\nfunction teardown() {\n // Reset mock services after each test\n variables.mockService.reset();\n}\n
"},"hint":"Callback that executes after every test case when using Wheels’ legacy testing framework. It is typically used to clean up any data, variables, or state changes made during a test, ensuring that each test runs in isolation and does not interfere with subsequent tests. This helps maintain reliability and consistency across the test suite.\n\n","returntype":"any","slug":"test.teardown","parameters":[],"availableIn":["test"],"name":"teardown","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. In a migration file\nt.text("description");\n\n2. Creates both summary and notes columns as text types\nt.text("summary,notes");\n\n3. Adds a column with a default placeholder text\nt.text(columnNames="details", default="N/A");\n\n4. Adds a text column that must always have a value\nt.text(columnNames="bio", allowNull=false);\n\n5. Adds a column with a default value and disallows nulls\nt.text(columnNames="comments", default="No comments provided", allowNull=false);\n
"},"hint":"Used within a migration to add one or more text columns to a database table definition. Text columns are designed for storing larger amounts of character data compared to standard string or varchar columns. This function allows you to define the column name, set a default value, and control whether the column allows null values.\n\n","returntype":"any","slug":"tabledefinition.text","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"text","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic textarea with label\n#textArea(label="Overview", objectName="article", property="overview")#\n\n2. Customizing with HTML attributes\n#textArea(\n label="Comments", \n objectName="post", \n property="comments", \n class="form-control", \n id="commentsBox", \n rows="5", \n cols="50"\n)#\n\n3. Using with nested associations\n<fieldset>\n <legend>Screenshots</legend>\n <cfloop from="1" to="#ArrayLen(site.screenshots)#" index="i">\n #fileField(label="File #i#", objectName="site", association="screenshots", position=i, property="file")#\n #textArea(label="Caption #i#", objectName="site", association="screenshots", position=i, property="caption")#\n </cfloop>\n</fieldset>\n\n4. Controlling label placement\n#textArea(\n label="Details", \n objectName="project", \n property="details", \n labelPlacement="before"\n)#\n\n5. Prepending and appending HTML\n#textArea(\n label="Notes", \n objectName="task", \n property="notes", \n prepend="<div class='input-wrapper'>", \n append="</div>"\n)#\n\n6. Handling validation errors\n#textArea(\n label="Description", \n objectName="product", \n property="description", \n errorElement="div", \n errorClass="input-error"\n)#\n
"},"hint":"Builds and returns an HTML <textarea> form control for a given model object and property. It is commonly used when you need a larger text input field, such as for descriptions, comments, or notes. The function automatically binds the value of the specified property from the object to the textarea. You can also pass additional attributes like class, id, or rel to customize the generated HTML. When working with nested forms or associations, you can specify the association and position arguments to bind the field to related objects. Wheels also provides options to add labels, control label placement, prepend or append HTML around the field, and handle error display automatically.\n\n","returntype":"string","slug":"controller.textArea","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textArea","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic textarea with label\n#textAreaTag(label="Description", name="description", content=params.description)#\n\n2. Textarea with custom attributes\n#textAreaTag(\n label="Notes", \n name="notes", \n class="form-control", \n id="notesBox", \n rows="6", \n cols="60"\n)#\n\n3. Textarea without label\n#textAreaTag(name="feedback", content="Enter your feedback here...")#\n\n4. Custom label placement\n#textAreaTag(\n label="Comments", \n name="comments", \n labelPlacement="before"\n)#\n\n5. Prepending and appending HTML\n#textAreaTag(\n label="Message", \n name="message", \n prepend="<div class='input-wrapper'>", \n append="</div>"\n)#\n
"},"hint":"Builds and returns an HTML <textarea> form control based only on the supplied field name, rather than being tied to a specific model object. It is useful when you want to generate a standalone text area not bound to an object, such as for ad-hoc forms, search boxes, or generic input fields. You can set the initial content of the textarea, add a label, and pass in additional attributes like class, id, or rel. Options are also available to control label placement, prepend or append HTML wrappers, and configure whether output should be encoded for XSS protection.\n\n","returntype":"string","slug":"controller.textAreaTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Content to display in textarea on page load.","name":"content","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textAreaTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic text field with label\n#textField(label="First Name", objectName="user", property="firstName")#\n\n2. Using a custom input type\n#textField(\n label="Email Address", \n objectName="user", \n property="email", \n type="email"\n)#\n\n3. Adding CSS classes and attributes\n#textField(\n label="Phone", \n objectName="contact", \n property="phoneNumber", \n class="form-control", \n placeholder="Enter phone number"\n)#\n\n4. Nested form with hasMany association\n<fieldset>\n <legend>Phone Numbers</legend>\n <cfloop from="1" to="#ArrayLen(contact.phoneNumbers)#" index="i">\n #textField(\n label="Phone ##i#", \n objectName="contact", \n association="phoneNumbers", \n position=i, \n property="phoneNumber"\n )#\n </cfloop>\n</fieldset>\n\n5. Prepending and appending HTML wrappers\n#textField(\n label="Website", \n objectName="company", \n property="website", \n prepend="<div class='field-wrapper'>", \n append="</div>"\n)#\n\n6. Handling validation errors\n#textField(\n label="Username", \n objectName="user", \n property="username", \n errorElement="div", \n errorClass="input-error"\n)#\n
"},"hint":"Builds and returns an HTML text field form control that is bound to a model object and one of its properties. By default, it will populate the value of the field from the property on the object. You can pass additional attributes such as class, id, or rel to customize the rendered tag. When working with nested associations or hasMany relationships, you can use the association and position arguments to bind the field to related properties. Wheels also supports automatically generating and placing labels, wrapping controls with custom HTML, and marking fields with errors when validation fails. The type argument lets you adjust the input type for use with HTML5 attributes like email, tel, or url.\n\n","returntype":"string","slug":"controller.textField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":"text","required":false,"hint":"Input type attribute. Common examples in HTML5 and later are text (default), email, tel, and url.","name":"type","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with label, name, and value\ntextFieldTag(label="Search", name="q", value=params.q)\n\n2. Email input with placeholder and custom class\ntextFieldTag(name="email", label="Email Address", type="email", class="form-control", placeholder="you@example.com")\n\n3. Label placed after the input\ntextFieldTag(name="username", label="Username", labelPlacement="after")\n\n4. Wrapped with prepend/append for Bootstrap styling\ntextFieldTag(name="price", label="Price", prepend="<div class='input-group'>", append="</div>", prependToLabel="<span class='icon'>$</span>")\n
"},"hint":"Builds and returns a string containing a text field form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.textFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"text","required":false,"hint":"Input type attribute. Common examples in HTML5 and later are text (default), email, tel, and url.","name":"type","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple time column\nt.time("startTime")\n\n2. Add multiple time columns\nt.time("opensAt, closesAt")\n\n3. Add a time column with a default value\nt.time(columnNames="reminderAt", default="09:00:00")\n\n4. Add a nullable time column\nt.time(columnNames="lunchBreak", allowNull=true)\n
"},"hint":"Adds one or more TIME columns to a table definition in a migration. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.time","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"time","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Example in a controller (outputs: "3 months")\naWhileAgo = DateAdd("d", -90, Now());\ntimeAgoInWords(aWhileAgo)\n\n2. Including seconds (outputs: "less than 5 seconds")\ntimeAgoInWords(DateAdd("s", -3, Now()), includeSeconds=true)\n\n3. Comparing two specific dates (Outputs: "5 months")\npast = CreateDateTime(2024, 01, 01, 12, 0, 0);\nfuture = CreateDateTime(2024, 06, 01, 12, 0, 0);\ntimeAgoInWords(fromTime=past, toTime=future)\n
"},"hint":"Returns a human-friendly string describing the approximate time difference between two dates (defaults to comparing against the current time).\n\n","returntype":"any","slug":"controller.timeAgoInWords","parameters":[{"required":true,"hint":"Date to compare from.","name":"fromTime","type":"date"},{"default":false,"required":false,"hint":"Whether or not to include the number of seconds in the returned string.","name":"includeSeconds","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Date to compare to.","name":"toTime","type":"date"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"timeAgoInWords","tags":{"categoryClass":"datefunctions","sectionClass":"globalhelpers","category":"Date Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: create hour, minute, and second selects for a property\ntimeSelect(objectName="business", property="openUntil")\n\n2. Only display hour and minute selectors\ntimeSelect(objectName="business", property="openUntil", order="hour,minute")\n\n3. Limit minutes to 15-minute intervals (00, 15, 30, 45)\ntimeSelect(objectName="appointment", property="dateTimeStart", minuteStep=15)\n\n4. Use 12-hour format with AM/PM\ntimeSelect(objectName="event", property="startTime", twelveHour=true)\n\n5. Add a blank option at the top\ntimeSelect(objectName="schedule", property="startTime", includeBlank="- Select Time -")\n\n6. Customize the label and append helper text\ntimeSelect(objectName="meeting", property="endTime", label="End Time", append="(select carefully)")\n
"},"hint":"Builds and returns three select form controls for hours, minutes, and seconds, based on the supplied object name and property. It is useful when you want users to input a time in a structured way without manually typing values. You can configure it to display only specific units (such as hours and minutes), control step intervals for minutes or seconds, display in 12-hour format with AM/PM, and customize labels, error handling, and additional HTML wrapping. By default, the three selects are ordered as hour, minute, and second, but you can change this order or exclude parts completely.\n\n","returntype":"string","slug":"controller.timeSelect","parameters":[{"default":"","required":false,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"default":"","required":false,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"order","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"separator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"timeSelect","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: creates hour, minute, and second selects\ntimeSelectTags(name="timeOfMeeting", selected=params.timeOfMeeting)\n\n2. Only show hour and minute selects\ntimeSelectTags(name="timeOfMeeting", selected=params.timeOfMeeting, order="hour,minute")\n\n3. Show 15-minute intervals\ntimeSelectTags(name="reminderTime", minuteStep=15)\n\n4. Display in 12-hour format with AM/PM\ntimeSelectTags(name="eventStart", twelveHour=true)\n\n5. Include a blank option\ntimeSelectTags(name="timeSlot", includeBlank="- Select Time -")\n\n6. Add a label and append helper text\ntimeSelectTags(name="appointmentEnd", label="End Time", append="(HH:MM:SS)")\n
"},"hint":"Builds and returns three <select> form controls for hours, minutes, and seconds based on the supplied name. This is the tag-based version of timeSelect(), meaning it does not bind to a model object but instead works with raw form field names. You can control the order of the selects, limit minute and second intervals, display in 12-hour format with AM/PM, and include custom labels, error handling, and HTML wrappers.\n\n","returntype":"string","slug":"controller.timeSelectTags","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"order","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"separator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"timeSelectTags","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a basic timestamp column\nt.timestamp("createdAt")\n\n2. Add multiple timestamp columns\nt.timestamp("createdAt, updatedAt")\n\n3. Add a timestamp column with a default value\nt.timestamp(columnNames="createdAt", default="CURRENT_TIMESTAMP")\n\n4. Add a nullable timestamp column\nt.timestamp(columnNames="deletedAt", allowNull=true)\n\n5. Override column type to use TIMESTAMP instead of DATETIME\nt.timestamp(columnNames="syncedAt", columnType="timestamp")\n
"},"hint":"Used to add one or more TIMESTAMP (or DATETIME) columns to a table definition. It lets you specify default values, whether the column allows NULL, and even override the underlying SQL type through the columnType argument. This is especially useful when you need to track creation and update times or work with custom timestamp fields. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.timestamp","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"},{"default":"datetime","required":false,"name":"columnType","type":"string"}],"availableIn":["tabledefinition"],"name":"timestamp","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add createdAt, updatedAt, and deletedAt columns to the users table\nt.timestamps()
"},"hint":"Shortcut for adding Wheels’ convention-based automatic timestamp and soft delete columns to a table definition during migrations. Instead of defining each field manually, this function quickly sets up the standard fields that are commonly used across models to track record lifecycle and soft deletion. By default, it adds createdAt, updatedAt, and deletedAt columns with appropriate types, making your migrations more concise and consistent. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.timestamps","parameters":[],"availableIn":["tabledefinition"],"name":"timestamps","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Example in a controller (outputs: "about 1 year")\naLittleAhead = DateAdd("d", 365, Now());\ntimeUntilInWords(aLittleAhead)\n\n2. Including seconds (outputs: "less than 5 seconds")\ntimeUntilInWords(DateAdd("s", 3, Now()), includeSeconds=true)\n\n3. Comparing between two specific dates (Outputs: "5 months")\nfromDate = CreateDateTime(2024, 01, 01, 12, 0, 0);\ntoDate = CreateDateTime(2024, 06, 01, 12, 0, 0);\ntimeUntilInWords(toTime=toDate, fromTime=fromDate)\n
"},"hint":"Returns a human-readable string describing the approximate time difference between the current date (or another starting point you provide) and a future date. It is the inverse of timeAgoInWords(), focusing on how long until something happens instead of how long ago it occurred. You can optionally include seconds for more precise descriptions.\n\n","returntype":"string","slug":"controller.timeUntilInWords","parameters":[{"required":true,"hint":"Date to compare to.","name":"toTime","type":"date"},{"default":false,"required":false,"hint":"Whether or not to include the number of seconds in the returned string.","name":"includeSeconds","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Date to compare from.","name":"fromTime","type":"date"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"timeUntilInWords","tags":{"categoryClass":"datefunctions","sectionClass":"globalhelpers","category":"Date Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\ntitleize(\"Wheels is a framework for ColdFusion\")\n// Output: \"Wheels Is A Framework For ColdFusion\"\n\n2. Works with single words\ntitleize(\"hello\")\n// Output: \"Hello\"\n\n3. Works with multiple words including numbers\ntitleize(\"coldfusion 2025 features\")\n// Output: \"Coldfusion 2025 Features\"\n\n4. Can be used in views for dynamic labels\n<h1>titleize(article.title)</h1>
"},"hint":"Converts a string so that the first letter of each word is capitalized, producing a cleaner, title-like appearance. It is useful for formatting headings, labels, or any text that should follow title case conventions.\n\n","returntype":"string","slug":"controller.titleize","parameters":[{"required":true,"hint":"The text to turn into a title.","name":"word","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"titleize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Fetch a user object and toggle a boolean property\nuser = model(\"user\").findByKey(58);\nisSuccess = user.toggle(\"isActive\");\n// Returns true if saved successfully, false otherwise\n\n2. Disable automatic saving\nuser = model(\"user\").findByKey(58);\nuser.toggle(property=\"isActive\", save=false);\n// Returns the user object without saving\n\n3. Use a dynamic helper method for convenience\nuser = model(\"user\").findByKey(58);\nisSuccess = user.toggleIsActive();\n// Returns whether the save was successful
"},"hint":"Assigns to the property specified the opposite of the property's current boolean value.\nThrows an error if the property cannot be converted to a boolean value.\nReturns this object if save called internally is false.\n\n","returntype":"boolean","slug":"model.toggle","parameters":[{"required":true,"name":"property","type":"string"},{"default":true,"required":false,"hint":"Argument to decide whether save the property after it has been toggled.","name":"save","type":"boolean"}],"availableIn":["model"],"name":"toggle","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Truncate text to 20 characters, default truncation string \"...\"\ntruncate(text=\"Wheels is a framework for ColdFusion\", length=20)\n/* Output: \"Wheels is a fra...\" */\n\n2. Use a custom truncation string\ntruncate(text=\"Wheels is a framework for ColdFusion\", truncateString=\" (more)\")\n/* Output: \"Wheels is a framework (more)\" */\n\n3. Short text does not get truncated\ntruncate(text=\"Short text\", length=20)\n/* Output: \"Short text\" */\n\n4. Display in a view for previews\n<p>truncate(article.content, 100)</p>
"},"hint":"Shortens a given text string to a specified length and appends a replacement string (by default \"...\") at the end to indicate truncation. It is useful for displaying previews of longer text in UIs, summaries, or reports while keeping the output concise.\n\n","returntype":"string","slug":"controller.truncate","parameters":[{"required":true,"hint":"The text to truncate.","name":"text","type":"string"},{"default":30,"required":false,"hint":"Length to truncate the text to.","name":"length","type":"numeric"},{"default":"...","required":false,"hint":"String to replace the last characters with.","name":"truncateString","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"truncate","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single UUID column\nt.uniqueidentifier("uuid")\n\n2. Add multiple UUID columns\nt.uniqueidentifier("uuid, externalId")\n\n3. Add a UUID column with default UUID generation\nt.uniqueidentifier(columnNames="uuid", default="newid()")\n\n4. Add a nullable UUID column\nt.uniqueidentifier(columnNames="optionalUuid", allowNull=true)\n
"},"hint":"Used to add one or more UUID (Universally Unique Identifier) columns to a table definition. These columns are useful for generating globally unique keys for records instead of relying on auto-incrementing integers. By default, the function uses newid() to populate the column with a UUID, and you can also configure whether the column allows NULL. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.uniqueidentifier","parameters":[{"required":false,"name":"columnNames","type":"string"},{"default":"newid()","required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"uniqueidentifier","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function up() {\n\ttransaction {\n\t\ttry {\n\t\t\t// your code goes here\n\t\t\tt = createTable(name='myTable');\n\t\t\tt.timestamps();\n\t\t\tt.create();\n\t\t} catch (any e) {\n\t\t\tlocal.exception = e;\n\t\t}\n\n\t\tif (StructKeyExists(local, "exception")) {\n\t\t\ttransaction action="rollback";\n\t\t\tthrow(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any");\n\t\t} else {\n\t\t\ttransaction action="commit";\n\t\t}\n\t}\n}\n
"},"hint":"Defines the actions to migrate your database schema forward. It is called when applying a migration and is typically paired with the down() function, which rolls back the migration. All schema changes, such as creating tables, adding columns, or setting up indexes, should be placed inside up(). Wrapping your migration code in a transaction block ensures that changes are either fully applied or rolled back in case of errors. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.up","parameters":[],"availableIn":["migration"],"name":"up","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get a post object and then update its title in the database\npost = model("post").findByKey(33);\npost.update(title="New version of Wheels just released");\n\n2. Get a post object and then update its title and other properties based on what is pased in from the URL/form\npost = model("post").findByKey(params.key);\npost.update(title="New version of Wheels just released", properties=params.post);\n\n3. If you have a `hasOne` association setup from `author` to `bio`, you can do a scoped call. (The `setBio` method below will call `bio.update(authorId=anAuthor.id)` internally.)\nauthor = model("author").findByKey(params.authorId); \nbio = model("bio").findByKey(params.bioId); \nauthor.setBio(bio); \n\n4. If you have a `hasMany` association setup from `owner` to `car`, you can do a scoped call. (The `addCar` method below will call `car.update(ownerId=anOwner.id)` internally.)\nanOwner = model("owner").findByKey(params.ownerId); \naCar = model("car").findByKey(params.carId); \nanOwner.addCar(aCar); \n\n5. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `removeComment` method below will call `comment.update(postId="")` internally.)\naPost = model("post").findByKey(params.postId); \naComment = model("comment").findByKey(params.commentId); \naPost.removeComment(aComment); // Get an object, and toggle a boolean property\nuser = model("user").findByKey(58); \nisSuccess = user.toggle("isActive"); // returns whether the object was saved properly\n\n// You can also use a dynamic helper for this\nisSuccess = user.toggleIsActive(); \n\n
"},"hint":"Updates an existing model object with the supplied properties and saves the changes to the database. It returns true if the save was successful and false otherwise. You can pass properties directly as named arguments or as a struct. Additional options allow you to control validation, callbacks, transactions, parameterization, cache reloading, and explicit timestamp handling. This method also works seamlessly with associations, making it possible to update related objects in hasOne or hasMany relationships.\n\n","returntype":"boolean","slug":"model.update","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to allow explicit assignment of `createdAt` or `updatedAt` properties","name":"allowExplicitTimestamps","type":"boolean"}],"availableIn":["model"],"name":"update","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Update all posts that are unpublished\nrecordsUpdated = model("post").updateAll(\n published=1,\n publishedAt=Now(),\n where="published=0"\n)\n\n2. Scoped update for a hasMany association (removing all comments)\npost = model("post").findByKey(params.postId);\npost.removeAllComments(); \n// Internally calls: model("comment").updateAll(postId="", where="postId=#post.id#")\n\n3. Update all users and force validations and callbacks\nrecordsUpdated = model("user").updateAll(\n properties={isActive=true},\n instantiate=true,\n validate=true\n)\n\n4. Using index hints for MySQL\nrecordsUpdated = model("user").updateAll(\n properties={isVerified=true},\n where="isVerified=0",\n useIndex={user="idx_users_isVerified"}\n)\n
"},"hint":"Updates all properties for the records that match the where argument.\nProperty names and values can be passed in either using named arguments or as a struct to the properties argument.\nBy default, objects will not be instantiated and therefore callbacks and validations are not invoked.\nYou can change this behavior by passing in instantiate=true.\nThis method returns the number of records that were updated.\n\n","returntype":"numeric","slug":"model.updateAll","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Whether or not to instantiate the object(s) first. When objects are not instantiated, any callbacks and validations set on them will be skipped.","name":"instantiate","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"updateAll","tags":{"categoryClass":"updatefunctions","sectionClass":"modelclass","category":"Update Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Update a record by key using a struct of properties\nresult = model("post").updateByKey(33, params.post);\n// Returns true if the update was successful\n\n2. Update a record by key using named arguments\nresult = model("post").updateByKey(\n key=33,\n title="New version of Wheels just released",\n published=1\n)\n\n3. Include soft-deleted records in the update\nresult = model("user").updateByKey(\n key=42,\n properties={isActive=true},\n includeSoftDeletes=true\n)\n\n4. Disable validation and callbacks\nresult = model("post").updateByKey(\n key=33,\n properties={title="Force Update"},\n validate=false,\n callbacks=false\n)\n
"},"hint":"Finds the object with the supplied key and saves it (if validation permits it) with the supplied properties and / or named arguments.\nProperty names and values can be passed in either using named arguments or as a struct to the properties argument.\nReturns true if the object was found and updated successfully, false otherwise.\n\n","returntype":"boolean","slug":"model.updateByKey","parameters":[{"required":true,"hint":"Primary key value(s) of the record to fetch. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"updateByKey","tags":{"categoryClass":"updatefunctions","sectionClass":"modelclass","category":"Update Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Sets the `new` property to `1` on the most recently released product\nresult = model("product").updateOne(order="releaseDate DESC", new=1);\n\n2. If you have a `hasOne` association setup from `user` to `profile`, you can do a scoped call. (The `removeProfile` method below will call `model("profile").updateOne(where="userId=#aUser.id#", userId="")` internally.)\naUser = model("user").findByKey(params.userId);\naUser.removeProfile();
"},"hint":"Retrieves a single model object based on the supplied arguments and updates it with the specified properties. It returns true if an object was found and updated successfully, and false if no object matched the criteria or the update failed. This method is useful when you want to update a single record that matches a certain condition without fetching multiple records. By default, objects are not instantiated, so validations and callbacks are applied only if enabled. Additional options allow control over query ordering, transactions, cache reloading, index hints, and inclusion of soft-deleted records.\n\n","returntype":"boolean","slug":"model.updateOne","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"false","required":false,"name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"updateOne","tags":{"categoryClass":"updatefunctions","sectionClass":"modelclass","category":"Update Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Update a single property on an existing product\nproduct = model(\"product\").findByKey(56);\nproduct.updateProperty(\"new\", 1);\n\n2. Update a boolean flag without callbacks or validations\nuser = model(\"user\").findByKey(42);\nuser.updateProperty(property=\"isActive\", value=false, callbacks=false);
"},"hint":"Updates a single property on a model object and saves the record immediately without running the normal validation procedures. This method is particularly useful for quickly updating flags or boolean values on existing records where full validation is not necessary. You can control transaction behavior, parameterization, and callback execution when using this method.\n\n","returntype":"boolean","slug":"model.updateProperty","parameters":[{"required":false,"hint":"Name of the property to update the value for globally.","name":"property","type":"string"},{"required":false,"hint":"Value to set on the given property globally.","name":"value","type":"any"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"}],"availableIn":["model"],"name":"updateProperty","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Update the `active` column to 0 for all admin users in a migration\nupdateRecord(table=\"users\", where=\"admin = 1\", active=0);\n\n2. Update a specific product record by ID\nupdateRecord(table=\"products\", where=\"id = 42\", price=19.99, stock=100);
"},"hint":"Allows you to update an existing record in a database table directly from within a migration CFC. This function is particularly useful when you need to modify data as part of a schema migration, such as setting default values, correcting legacy data, or updating specific records based on certain conditions. The function requires the table name and optionally allows a where clause to target specific rows. Only available in a migrator CFC.\n\n","returntype":"void","slug":"migration.updateRecord","parameters":[{"required":true,"hint":"The table name where the record is","name":"table","type":"string"},{"default":"","required":false,"hint":"The where clause, i.e admin = 1","name":"where","type":"string"}],"availableIn":["migration"],"name":"updateRecord","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Create the URL for the `logOut` action on the `account` controller, typically resulting in `/account/log-out`\n#urlFor(controller="account", action="logOut")#\n\n2. Create a URL with an anchor set on it\n#urlFor(action="comments", anchor="comment10")#\n\n3. Create a URL based on a route called `products`, which expects params for `categorySlug` and `productSlug`\n#urlFor(route="product", categorySlug="accessories", productSlug="battery-charger")#
"},"hint":"Generates an internal URL based on the supplied arguments. It can create URLs using a named route, or by specifying a controller and action directly. Additional options let you include keys, query parameters, anchors, and override protocol, host, or port. By default, the function returns a relative URL, but you can configure it to return a fully qualified URL. URL parameters are automatically encoded for safety, but for HTML attribute safety, further encoding is recommended.\n\n","returntype":"string","slug":"controller.URLFor","parameters":[{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: `wheels=cool&x=y`). Please note that Wheels uses the `&` and `=` characters to split the parameters and encode them properly for you. However, if you need to pass in `&` or `=` as part of the value, then you need to encode them (and only them), example: `a=cats%26dogs%3Dtrouble!&b=1`.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If `true`, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"},{"default":false,"required":false,"name":"$encodeForHtmlAttribute","type":"boolean"},{"default":"[runtime expression]","required":false,"name":"$URLRewriting","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"URLFor","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. We want this layout to be used as the default throughout the entire controller, except for the `myAjax` action. \nusesLayout(template="myLayout", except="myAjax"); \n\n2. Use a custom layout for these actions but use the default `layout.cfm` for the rest. \nusesLayout(template="myLayout", only="termsOfService,shippingPolicy"); \n\n3. Define a custom function to decide which layout to display.\n// The `setLayout` function should return the name of the layout to use or `true` to use the default one. \nusesLayout("setLayout");
"},"hint":"Used inside a controller's config() function to specify which layout template should be applied to the controller or specific actions. You can define a default layout for the entire controller, specify layouts only for certain actions, exclude specific actions from using a layout, or even provide a custom function to determine which layout to use dynamically. This allows fine-grained control over your page structure and helps maintain consistent design while accommodating exceptions.\n\n","returntype":"void","slug":"controller.usesLayout","parameters":[{"required":true,"hint":"Name of the layout template or function name you want to use.","name":"template","type":"string"},{"default":"","required":false,"hint":"Name of the layout template you want to use for AJAX requests.","name":"ajax","type":"string"},{"required":false,"hint":"List of actions that should not get the layout.","name":"except","type":"string"},{"required":false,"hint":"List of actions that should only get the layout.","name":"only","type":"string"},{"default":true,"required":false,"hint":"When specifying conditions or a function, pass in `true` to use the default `layout.cfm` if none of the conditions are met.","name":"useDefault","type":"boolean"}],"availableIn":["controller"],"name":"usesLayout","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
// Check if a user is valid before proceeding with execution\nuser = model("user").new(params.user);\n\nif(user.valid()){\n    // Do something here\n}
"},"hint":"Runs the validation on the object and returns true if it passes it.\nWheels will run the validation process automatically whenever an object is saved to the database, but sometimes it's useful to be able to run this method to see if the object is valid without saving it to the database.\n\n","returntype":"boolean","slug":"model.valid","parameters":[{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"name":"validateAssociations","type":"boolean"}],"availableIn":["model"],"name":"valid","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Register a method to validate objects before saving\nfunction config() {\n validate("checkPhoneNumber");\n}\n\nfunction checkPhoneNumber() {\n // Make sure area code is '614'\n return Left(this.phoneNumber, 3) == "614";\n}\n\n2. Register multiple validation methods\nfunction config() {\n validate("checkPhoneNumber, checkEmailFormat");\n}\n\nfunction checkEmailFormat() {\n // Ensure email contains '@'\n return Find("@", this.email);\n}\n\n3. Conditional validation using `condition`\nfunction config() {\n // Only validate phone numbers if the user is in the US\n validate("checkPhoneNumber", condition="this.country == 'US'");\n}\n\n4. Skip validation under certain conditions using `unless`\nfunction config() {\n // Skip phone number validation if the user is a guest\n validate("checkPhoneNumber", unless="this.isGuest");\n}\n\n5. Run validation only on create or update\nfunction config() {\n // Validate email only when creating a new record\n validate("checkEmailFormat", when="onCreate");\n\n // Validate password only on update\n validate("checkPasswordStrength", when="onUpdate");\n}\n
"},"hint":"Used to register one or more validation methods that will be executed on a model object before it is saved to the database. This allows you to define custom validation logic beyond the built-in validations like presence or uniqueness. You can also control when the validation runs (on create, update, or both) and under what conditions using condition and unless.\n\n","returntype":"void","slug":"model.validate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names to call. Can also be called with the `method` argument.","name":"methods","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"}],"availableIn":["model"],"name":"validate","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Validate new objects before insertion\nfunction config() {\n validateOnCreate("checkPhoneNumber");\n}\n\nfunction checkPhoneNumber() {\n // Ensure area code is '614'\n return Left(this.phoneNumber, 3) == "614";\n}\n\n2. Register multiple methods for validation on creation\nfunction config() {\n validateOnCreate("checkPhoneNumber, checkEmailFormat");\n}\n\nfunction checkEmailFormat() {\n // Ensure email contains '@'\n return Find("@", this.email);\n}\n\n3. Conditional validation using `condition`\nfunction config() {\n // Only validate phone number if the country is US\n validateOnCreate("checkPhoneNumber", condition="this.country == 'US'");\n}\n\n4. Skip validation under certain conditions using `unless`\nfunction config() {\n // Skip phone number validation if user is a guest\n validateOnCreate("checkPhoneNumber", unless="this.isGuest");\n}\n
"},"hint":"Registers one or more validation methods that will be executed only when a new object is being inserted into the database. This is useful for rules that should apply strictly at creation and not during updates. You can also control whether the validation runs using the condition and unless arguments.\n\n","returntype":"void","slug":"model.validateOnCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names to call. Can also be called with the `method` argument.","name":"methods","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validateOnCreate","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: validate existing objects before update\nfunction config() {\n validateOnUpdate("checkPhoneNumber");\n}\n\nfunction checkPhoneNumber() {\n // Ensure area code is '614'\n return Left(this.phoneNumber, 3) == "614";\n}\n\n2. Register multiple methods for validation on update\nfunction config() {\n validateOnUpdate("checkPhoneNumber, checkEmailFormat");\n}\n\nfunction checkEmailFormat() {\n // Ensure email contains '@'\n return Find("@", this.email);\n}\n\n3. Conditional validation using `condition`\nfunction config() {\n // Only validate phone number if the country is US\n validateOnUpdate("checkPhoneNumber", condition="this.country == 'US'");\n}\n\n4. Skip validation under certain conditions using `unless`\nfunction config() {\n // Skip phone number validation if the user is an admin\n validateOnUpdate("checkPhoneNumber", unless="this.isAdmin");\n}\n
"},"hint":"Registers one or more validation methods that will be executed only when an existing object is being updated in the database. This allows you to enforce rules that apply strictly to updates, without affecting the creation of new records. You can also control whether the validation runs using the condition and unless arguments.\n\n","returntype":"void","slug":"model.validateOnUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names to call. Can also be called with the `method` argument.","name":"methods","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validateOnUpdate","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Validate password confirmation on user creation\nvalidatesConfirmationOf(\n    property="password",\n    when="onCreate",\n    message="Your password and its confirmation do not match. Please try again."\n);\n\n2. Validate multiple fields at once: password and email\nvalidatesConfirmationOf(\n    properties="password,email",\n    when="onCreate",\n    message="Fields must match their confirmation."\n);\n\n3. Case-sensitive validation\nvalidatesConfirmationOf(\n    property="password",\n    caseSensitive=true,\n    message="Password and confirmation must match exactly, including case."\n);\n\n4. Conditional validation using `condition`\nvalidatesConfirmationOf(\n    property="email",\n    condition="this.isNewsletterSubscriber",\n    message="Email confirmation required for newsletter subscription."\n);\n\n5. Skip validation for admin users using `unless`\nvalidatesConfirmationOf(\n    property="password",\n    unless="this.isAdmin",\n    message="Admins do not need to confirm their password."\n);\n
"},"hint":"Validates that the value of the specified property also has an identical confirmation value.\nThis is common when having a user type in their email address a second time to confirm, confirming a password by typing it a second time, etc.\nThe confirmation value only exists temporarily and never gets saved to the database.\nBy convention, the confirmation property has to be named the same as the property with \"Confirmation\" appended at the end.\nUsing the password example, to confirm our password property, we would create a property called passwordConfirmation.\n\n","returntype":"void","slug":"model.validatesConfirmationOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] should match confirmation","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":false,"required":false,"hint":"Ensure the confirmed property comparison is case sensitive","name":"caseSensitive","type":"boolean"}],"availableIn":["model"],"name":"validatesConfirmationOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Prevent users from selecting certain programming languages\nvalidatesExclusionOf(\n    property="coolLanguage",\n    list="php,fortran",\n    message="Haha, you can not be serious. Try again, please."\n);\n\n2. Validate multiple properties at once\nvalidatesExclusionOf(\n    properties="username,email",\n    list="admin,root,system",\n    message="This value is reserved. Please choose another."\n);\n\n3. Only apply validation on object creation\nvalidatesExclusionOf(\n    property="username",\n    list="admin,root",\n    when="onCreate",\n    message="Username is reserved and cannot be used."\n);\n\n4. Skip validation if the property is blank\nvalidatesExclusionOf(\n    property="nickname",\n    list="boss,chief",\n    allowBlank=true\n);\n\n5. Conditional validation using `condition`\nvalidatesExclusionOf(\n    property="category",\n    list="deprecated,legacy",\n    condition="this.isArchived",\n    message="Archived items cannot use deprecated categories."\n);\n\n6. Skip validation for admin users using `unless`\nvalidatesExclusionOf(\n    property="role",\n    list="banned,guest",\n    unless="this.isAdmin",\n    message="This role is restricted for regular users."\n);\n
"},"hint":"Ensures that the value of a specified property is not included in a given list of disallowed values. This is commonly used to prevent reserved words, restricted entries, or disallowed values from being saved to the database. You can specify when the validation should run, allow blank values to skip validation, or conditionally run it.\n\n","returntype":"void","slug":"model.validatesExclusionOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"required":true,"hint":"Single value or list of values that should not be allowed.","name":"list","type":"string"},{"default":"[property] is reserved","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesExclusionOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Validate that a credit card number is correct\nvalidatesFormatOf(property="cc", type="creditcard");\n\n2. Validate that a US zipcode matches 5 or 9 digit format\nvalidatesFormatOf(property="zipcode", type="zipcode");\n\n3. Ensure that an email ends with `.se` when IP check returns true and today is not Sunday\nvalidatesFormatOf(\n    property="email",\n    regEx="^.*@.*\\.se$",\n    condition="ipCheck()",\n    unless="DayOfWeek() eq 1",\n    message="Sorry, you must have a Swedish email address to use this website."\n);\n\n4. Validate that a username contains only letters, numbers, or underscores\nvalidatesFormatOf(\n    property="username",\n    regEx="^[a-zA-Z0-9_]+$",\n    message="Username can only contain letters, numbers, and underscores."\n);\n\n5. Validate multiple properties at once using built-in CFML types\nvalidatesFormatOf(\n    properties="phone,email",\n    type="telephone,email",\n    allowBlank=true\n);\n\n6. Validate only when updating an existing object\nvalidatesFormatOf(\n    property="ssn",\n    type="social_security_number",\n    when="onUpdate",\n    message="Invalid SSN format for updating records."\n);\n
"},"hint":"Validates that the value of the specified property is formatted correctly by matching it against a regular expression using the regEx argument and / or against a built-in CFML validation type using the type argument (creditcard, date, email, etc.).\n\n","returntype":"void","slug":"model.validatesFormatOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"","required":false,"hint":"Regular expression to verify against.","name":"regEx","type":"string"},{"default":"","required":false,"hint":"One of the following types to verify against: creditcard, date, email, eurodate, guid, social_security_number, ssn, telephone, time, URL, USdate, UUID, variableName, zipcode (will be passed through to your CFML engine's IsValid() function).","name":"type","type":"string"},{"default":"[property] is invalid","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesFormatOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure that the user selects either "Wheels" or "Rails" as their framework\nvalidatesInclusionOf(\n    property="frameworkOfChoice",\n    list="wheels,rails",\n    message="Please try again, and this time, select a decent framework!"\n);\n\n2. Validate multiple properties at once\nvalidatesInclusionOf(\n    properties="frameworkOfChoice,editorChoice",\n    list="wheels,rails,vsCode,sublime",\n    message="Invalid selection."\n);\n\n3. Only validate when creating a new object\nvalidatesInclusionOf(\n    property="subscriptionType",\n    list="free,premium,enterprise",\n    when="onCreate",\n    message="You must choose a valid subscription type."\n);\n\n4. Skip validation if property is blank\nvalidatesInclusionOf(\n    property="preferredLanguage",\n    list="cfml,python,javascript",\n    allowBlank=true\n);\n\n5. Conditionally validate only for users in Europe\nvalidatesInclusionOf(\n    property="currency",\n    list="EUR,GBP,CHF",\n    condition="this.region eq 'Europe'",\n    message="Invalid currency for European users."\n);\n
"},"hint":"Ensures that a property’s value exists in a predefined list of allowed values. It is commonly used for dropdowns, radio buttons, or any scenario where only specific values are acceptable.\n\n","returntype":"void","slug":"model.validatesInclusionOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"required":true,"hint":"List of allowed values.","name":"list","type":"string"},{"default":"[property] is not included in the list","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesInclusionOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure that `firstName` and `lastName` are no more than 50 characters\nvalidatesLengthOf(\n    properties="firstName,lastName",\n    maximum=50,\n    message="Please shorten your [property] please (50 characters max)."\n);\n\n2. Ensure `password` is between 4 and 20 characters\nvalidatesLengthOf(\n    property="password",\n    within="4,20",\n    message="The password length must be between 4 and 20 characters."\n);\n\n3. Ensure `username` is exactly 8 characters\nvalidatesLengthOf(\n    property="username",\n    exactly=8,\n    message="Username must be exactly 8 characters."\n);\n\n4. Only validate if `region` is 'US'\nvalidatesLengthOf(\n    property="zipCode",\n    exactly=5,\n    condition="this.region eq 'US'",\n    message="US zip codes must be exactly 5 digits."\n);\n\n5. Skip validation if property is blank\nvalidatesLengthOf(\n    property="nickname",\n    maximum=15,\n    allowBlank=true\n);\n
"},"hint":"Validates that the value of the specified property matches the length requirements supplied.\nUse the exactly, maximum, minimum and within arguments to specify the length requirements.\n\n","returntype":"void","slug":"model.validatesLengthOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] is the wrong length","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":0,"required":false,"hint":"The exact length that the property value must be.","name":"exactly","type":"numeric"},{"default":0,"required":false,"hint":"The maximum length that the property value can be.","name":"maximum","type":"numeric"},{"default":0,"required":false,"hint":"The minimum length that the property value can be.","name":"minimum","type":"numeric"},{"default":"","required":false,"hint":"A list of two values (minimum and maximum) that the length of the property value must fall within.","name":"within","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesLengthOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Score must be an integer, but allow blank values\nvalidatesNumericalityOf(\n    property="score",\n    onlyInteger=true,\n    allowBlank=true,\n    message="Please enter a correct score."\n);\n\n2. Age must be a number greater than or equal to 18\nvalidatesNumericalityOf(\n    property="age",\n    greaterThanOrEqualTo=18,\n    message="You must be at least 18 years old."\n);\n\n3. Price must be a positive number less than 1000\nvalidatesNumericalityOf(\n    property="price",\n    greaterThan=0,\n    lessThan=1000,\n    message="Price must be between 0 and 1000."\n);\n\n4. Ensure a number is odd and an integer\nvalidatesNumericalityOf(\n    property="lotteryNumber",\n    odd=true,\n    onlyInteger=true,\n    message="Lottery number must be an odd integer."\n);\n\n5. Validate only when a specific condition is true\nvalidatesNumericalityOf(\n    property="discount",\n    greaterThanOrEqualTo=0,\n    lessThanOrEqualTo=50,\n    condition="this.isOnSale()",\n    message="Discount must be between 0 and 50 for sale items."\n);\n
"},"hint":"Ensures that a property’s value is numeric. You can also enforce additional constraints such as integer-only values, odd/even numbers, and comparison limits (greaterThan, lessThan, etc.).\n\n","returntype":"void","slug":"model.validatesNumericalityOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] is not a number","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":false,"required":false,"hint":"Specifies whether the property value must be an integer.","name":"onlyInteger","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":"","required":false,"name":"odd","type":"boolean"},{"default":"","required":false,"name":"even","type":"boolean"},{"default":"","required":false,"hint":"Specifies whether or not the value must be greater than the supplied value.","name":"greaterThan","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be greater than or equal the supplied value.","name":"greaterThanOrEqualTo","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be equal to the supplied value.","name":"equalTo","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be less than the supplied value.","name":"lessThan","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be less than or equal the supplied value.","name":"lessThanOrEqualTo","type":"numeric"}],"availableIn":["model"],"name":"validatesNumericalityOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure the `emailAddress` property is not blank\nvalidatesPresenceOf("emailAddress");\n\n2. Ensure multiple properties are present\nvalidatesPresenceOf("firstName,lastName,emailAddress");\n\n3. Use a custom error message for missing email\nvalidatesPresenceOf(\n    property="emailAddress",\n    message="Email is required to create your account."\n);\n\n4. Validate only on create, not on update\nvalidatesPresenceOf(\n    properties="password",\n    when="onCreate",\n    message="Password is required when registering a new user."\n);\n\n5. Conditional validation based on a method\nvalidatesPresenceOf(\n    properties="discountCode",\n    condition="this.isOnSale()",\n    message="Discount code must be present for sale items."\n);\n
"},"hint":"Ensures that the specified property (or properties) exists and is not blank. It is commonly used to enforce required fields before saving an object to the database.\n\n","returntype":"void","slug":"model.validatesPresenceOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] can't be empty","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesPresenceOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure that usernames are unique across all users\nvalidatesUniquenessOf(\n    property="username",\n    message="Sorry, that username is already taken."\n);\n\n2. Ensure that email addresses are unique\nvalidatesUniquenessOf(\n    property="emailAddress",\n    message="This email has already been registered."\n);\n\n3. Allow the same username in different accounts but unique within an account\nvalidatesUniquenessOf(\n    property="username",\n    scope="accountId",\n    message="This username is already used in this account."\n);\n\n4. Only enforce uniqueness if the user is active\nvalidatesUniquenessOf(\n    property="username",\n    condition="this.isActive",\n    message="Active users must have a unique username."\n);\n\n5. Skip uniqueness check if the field is blank\nvalidatesUniquenessOf(\n    property="nickname",\n    allowBlank=true,\n    message="Nickname must be unique if supplied."\n);\n
"},"hint":"Validates that the value of the specified property is unique in the database table.\nUseful for ensuring that two users can't sign up to a website with identical usernames for example.\nWhen a new record is created, a check is made to make sure that no record already exists in the database table with the given value for the specified property.\nWhen the record is updated, the same check is made but disregarding the record itself.\n\n","returntype":"void","slug":"model.validatesUniquenessOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] has already been taken","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"One or more properties by which to limit the scope of the uniqueness constraint.","name":"scope","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":"true","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"validatesUniquenessOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Create a new employee object\nemployee = model("employee").new();\n\n1. Assume 'firstName' is a varchar(50) column\nemployee.validationTypeForProperty("firstName") (This will output: "string")\n\n2. Assume 'hireDate' is a datetime column\nemployee.validationTypeForProperty("hireDate") (This will output: "date")\n\n3. Assume 'salary' is a numeric column\nemployee.validationTypeForProperty("salary") (This will output: "numeric")\n
"},"hint":"Returns the type of validation that Wheels would apply for a given property. This is useful if you want to dynamically inspect a model's property type or apply logic based on the property's expected format.\n\n","returntype":"any","slug":"model.validationTypeForProperty","parameters":[{"required":true,"hint":"Name of column to retrieve data for.","name":"property","type":"string"}],"availableIn":["model"],"name":"validationTypeForProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get verification chain, remove the first item, and set it back.\nmyVerificationChain = verificationChain();\nArrayDeleteAt(myVerificationChain, 1);\nsetVerificationChain(myVerificationChain);\n
"},"hint":"Returns an array of all verifications (filters, before-actions, or checks) that are configured for the current controller, in the order they will be executed. This allows you to inspect, modify, or reorder the verifications dynamically.\n\n","returntype":"array","slug":"controller.verificationChain","parameters":[],"availableIn":["controller"],"name":"verificationChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Tell Wheels to verify that the `handleForm` action is always a `POST` request when executed.\nverifies(only="handleForm", post=true);\n\n2. Make sure that the edit action is a `GET` request, that `userId` exists in the `params` struct, and that it's an integer.\nverifies(only="edit", get=true, params="userId", paramsTypes="integer");\n\n3. Just like above, only this time we want to invoke a custom function in our controller to handle the request when it is invalid.\nverifies(only="edit", get=true, params="userId", paramsTypes="integer", handler="myCustomFunction");\n\n4. Just like above, only this time instead of specifying a handler, we want to `redirect` the visitor to the index action of the controller and show an error in The Flash when the request is invalid.\nverifies(only="edit", get=true, params="userId", paramsTypes="integer", action="index", error="Invalid userId");\n
"},"hint":"Instructs a Wheels controller to check that certain criteria are met before executing an action. This is useful for enforcing request types, required parameters, session/cookie values, or custom verifications. Note that all undeclared arguments will be passed to redirectTo() call if a handler is not specified.\n\n","returntype":"void","slug":"controller.verifies","parameters":[{"default":"","required":false,"hint":"List of action names to limit this verification to.","name":"only","type":"string"},{"default":"","required":false,"hint":"List of action names to exclude this verification from.","name":"except","type":"string"},{"default":"","required":false,"hint":"Set to true to verify that this is a `POST` request.","name":"post","type":"any"},{"default":"","required":false,"hint":"Set to true to verify that this is a `GET` request.","name":"get","type":"any"},{"default":"","required":false,"hint":"Set to true to verify that this is an `AJAX` request.","name":"ajax","type":"any"},{"default":"","required":false,"hint":"Verify that the passed in variable name exists in the cookie scope.","name":"cookie","type":"string"},{"default":"","required":false,"hint":"Verify that the passed in variable name exists in the session scope.","name":"session","type":"string"},{"default":"","required":false,"hint":"Verify that the passed in variable name exists in the params struct.","name":"params","type":"string"},{"default":"","required":false,"hint":"Pass in the name of a function that should handle failed verifications. The default is to just abort the request when a verification fails.","name":"handler","type":"string"},{"default":"","required":false,"hint":"List of types to check each listed cookie value against (will be passed through to your CFML engine's `IsValid` function).","name":"cookieTypes","type":"string"},{"default":"","required":false,"hint":"List of types to check each list session value against (will be passed through to your CFML engine's `IsValid` function).","name":"sessionTypes","type":"string"},{"default":"","required":false,"hint":"List of types to check each params value against (will be passed through to your CFML engine's `IsValid` function).","name":"paramsTypes","type":"string"}],"availableIn":["controller"],"name":"verifies","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Enables `[controller]` and `[controller]/[action]`, only via `GET` requests.\n    .wildcard()\n\n    // Enables `[controller]/[action]/[key]` as well.\n    .wildcard(mapKey=true)\n\n    // Also enables patterns like `[controller].[format]` and\n    // `[controller]/[action].[format]`\n    .wildcard(mapFormat=true)\n\n    // Allow additional methods beyond just `GET`\n    //\n    // Note that this can open some serious security holes unless you use `verifies`\n    // in the controller to make sure that requests changing data can only occur\n    // with a `POST` method.\n    .wildcard(methods="get,post")\n.end();\n\n</cfscript>
"},"hint":"Automatically generates dynamic routes for your controllers using placeholders like [controller], [action], and optionally [key] or [format]. This allows you to quickly map standard URL patterns to controllers and actions without explicitly defining every route.\n","returntype":"struct","slug":"mapper.wildcard","parameters":[{"default":"get","required":false,"hint":"List of HTTP methods (verbs) to generate the wildcard routes for. We strongly recommend leaving the default value of `get` and using other routing mappers if you need to `POST` to a URL endpoint. For better readability, you can also pass this argument as `methods` if you're listing multiple methods.","name":"method","type":"string"},{"default":"index","required":false,"hint":"Default action to specify if the value for the `[action]` placeholder is not provided.","name":"action","type":"string"},{"default":false,"required":false,"hint":"Whether or not to enable a `[key]` matcher, enabling a `[controller]/[action]/[key]` pattern.","name":"mapKey","type":"boolean"},{"default":false,"required":false,"hint":"Whether or not to add an optional `.[format]` pattern to the end of the generated routes. This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"wildcard","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic truncation (default truncate string "...")\nwordTruncate(text="Wheels is a framework for ColdFusion", length=4)\n// Output:\n// Wheels is a framework...\n\n2. Truncate with a custom string\nwordTruncate(text="Wheels is a framework for ColdFusion", length=3, truncateString=" (more)")\n// Output:\n// Wheels is a (more)\n\n3. Using with shorter text than length (no truncation applied)\nwordTruncate(text="Hello world", length=5)\n// Output:\n// Hello world\n\n4. Dynamic usage in a view\n<cfoutput>\n    wordTruncate(text=post.content, length=10)\n</cfoutput>\n// Useful for showing previews of long content while preserving word boundaries.\n
"},"hint":"Truncates text to the specified length of words and replaces the remaining characters with the specified truncate string (which defaults to \"...\").\n\n","returntype":"string","slug":"controller.wordTruncate","parameters":[{"required":true,"hint":"The text to truncate.","name":"text","type":"string"},{"default":5,"required":false,"hint":"Number of words to truncate the text to.","name":"length","type":"numeric"},{"default":"...","required":false,"hint":"String to replace the last characters with.","name":"truncateString","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"wordTruncate","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic year dropdown\nyearSelectTag(name="yearOfBirthday", selected=params.yearOfBirthday)\n\n2. Custom range (past 50 years, at least 18 years ago)\nfiftyYearsAgo = Year(Now()) - 50;\neighteenYearsAgo = Year(Now()) - 18;\n\nyearSelectTag(\n    name="yearOfBirthday",\n    selected=params.yearOfBirthday,\n    startYear=fiftyYearsAgo,\n    endYear=eighteenYearsAgo\n)\n\n3. Include a blank option\nyearSelectTag(name="graduationYear", includeBlank="- Select Year -")\n\n4. Add label with custom placement\nyearSelectTag(\n    name="yearOfHiring",\n    label="Hiring Year",\n    labelPlacement="aroundRight"\n)\n
"},"hint":"Builds and returns a string containing a select form control for a range of years based on the supplied name.\n\n","returntype":"string","slug":"controller.yearSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The year that should be selected initially.","name":"selected","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"yearSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}}]} \ No newline at end of file +{"sections":[{"categories":["Miscellaneous Functions","Routing"],"name":"Configuration"},{"categories":["Configuration Functions","Flash Functions","Miscellaneous Functions","Pagination Functions","Provides Functions","Rendering Functions"],"name":"Controller"},{"categories":["Date Functions","Miscellaneous Functions","String Functions"],"name":"Global Helpers"},{"categories":["General Functions","Migration Functions","Table Definition Functions"],"name":"Migrator"},{"categories":["Create Functions","CRUD Functions","Delete Functions","Miscellaneous Functions","Read Functions","Statistics Functions","Update Functions"],"name":"Model Class"},{"categories":["Association Functions","Callback Functions","Miscellaneous Functions","Validation Functions"],"name":"Model Configuration"},{"categories":["Change Functions","CRUD Functions","Error Functions","Miscellaneous Functions"],"name":"Model Object"},{"categories":["Callback Functions"],"name":"Test Model Configuration"},{"categories":["Testing Functions"],"name":"Test Model"},{"categories":["Asset Functions","Error Functions","Form Association Functions","Form Object Functions","Form Tag Functions","General Form Functions","Link Functions","Miscellaneous Functions","Sanitization Functions"],"name":"View Helpers"}],"functions":[{"extended":{"hasExtended":true,"docs":"
1. Allow only one property\n// In app/models/User.cfc\nfunction config() {\n    // Only allow `isActive` to be set through mass assignment\n    accessibleProperties("isActive");\n}\n\n// Example usage\nUser.updateAll(isActive=true);\n\n2. Allow multiple properties\n// In app/models/User.cfc\nfunction config() {\n    // Allow name and email to be set\n    accessibleProperties("firstName,lastName,email");\n}\n\n// Example usage\nUser.create(firstName="new", lastName="user", email="new@example.com");\n\n3. Dynamic restriction per model\n// In app/models/Post.cfc\nfunction config() {\n    if (application.env.environment == "production") {\n        // Lock down sensitive fields in production\n        accessibleProperties("title,content");\n    } else {\n        // In dev, keep it open for testing\n    }\n}
"},"hint":"Use this method inside your model’s config() function to whitelist which properties can be set via mass assignment operations (such as updateAll(), updateOne() and etc). This helps protect your model from accidental or malicious updates to sensitive fields (e.g., isAdmin, passwordHash, etc.).\n\n","returntype":"void","slug":"model.accessibleProperties","parameters":[{"default":"","required":false,"hint":"Property name (or list of property names) that are allowed to be altered through mass assignment.","name":"properties","type":"string"}],"availableIn":["model"],"name":"accessibleProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple string column\naddColumn(\n    table="members",\n    columnType="string",\n    columnName="status",\n    limit=50\n);\n\nAdds a status column (string, max 50 chars) to the members table.\n\n2. Add an integer column with default value\naddColumn(\n    table="orders",\n    columnType="integer",\n    columnName="priority",\n    default=0\n);\n\nAdds a priority column with default value 0.\n\n3. Add a boolean column that does not allow NULL\naddColumn(\n    table="users",\n    columnType="boolean",\n    columnName="isActive",\n    allowNull=false,\n    default=1\n);\n\nAdds an isActive column with default value true (1), disallowing NULL.\n\n4. Add a decimal column with precision and scale\naddColumn(\n    table="products",\n    columnType="decimal",\n    columnName="price",\n    precision=10,\n    scale=2\n);\n\nAdds a price column with up to 10 digits total, including 2 decimal places.\n\n5. Add a reference (foreign key) column\naddColumn(\n    table="orders",\n    columnType="reference",\n    columnName="userId",\n    referenceName="users"\n);\n\nAdds a userId column to orders and links it to the users table.
"},"hint":"Adds a new column to an existing table.\n This function is only available inside a migration CFC and is part of the Wheels migrator API. Use it to evolve your database schema safely through versioned migrations.\n\n","returntype":"void","slug":"migration.addColumn","parameters":[{"required":true,"hint":"The Name of the table to add the column to","name":"table","type":"string"},{"required":true,"hint":"The type of the new column","name":"columnType","type":"string"},{"default":"","required":true,"hint":"THe name of the new column","name":"columnName","type":"string"},{"default":"","required":false,"hint":"The name of the column which this column should be inserted after","name":"afterColumn","type":"string"},{"default":"","required":false,"hint":"Name for new reference column, see documentation for references function, required if columnType is 'reference'","name":"referenceName","type":"string"},{"required":false,"hint":"Default value for this column","name":"default","type":"string"},{"required":false,"hint":"Whether to allow NULL values","name":"allowNull","type":"boolean"},{"required":false,"hint":"Character or integer size limit for column","name":"limit","type":"numeric"},{"required":false,"hint":"precision value for decimal columns, i.e. number of digits the column can hold","name":"precision","type":"numeric"},{"required":false,"hint":"scale value for decimal columns, i.e. number of digits that can be placed to the right of the decimal point (must be less than or equal to precision)","name":"scale","type":"numeric"}],"availableIn":["migration"],"name":"addColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple error\n// In app/models/User.cfc\nthis.addError(\n    property="email",\n    message="Sorry, you are not allowed to use that email. Try again, please."\n);\n\nAdds an error on the email property.\n\n2. Add an error with a name identifier\nthis.addError(\n    property="password",\n    message="Password must contain at least one special character.",\n    name="weakPassword"\n);\n\nAdds a weakPassword error on the password property.\nLater you can check for it:\n\nif (user.hasError("password", "weakPassword")) {\n    // Handle specifically the weak password case\n}\n\n3. Adding multiple errors to the same property\nthis.addError(property="username", message="Username already taken.", name="duplicate");\nthis.addError(property="username", message="Username cannot contain spaces.", name="invalidChars");\n\nTwo different errors on username, each distinguished by their name.\n\n4. Conditional custom errors\n// Suppose only company emails are allowed\nif (!listLast(this.email, "@") == "company.com") {\n    this.addError(\n        property="email",\n        message="Please use your company email address.",\n        name="invalidDomain"\n    );\n}\n\nCustom rule ensures only company domain emails are accepted.\n\n5. Combine with built-in validations\n// Inside a callback\nfunction beforeSave() {\n    if (this.age < 18) {\n        this.addError(property="age", message="You must be at least 18 years old.");\n    }\n}\n\nEven though validatesPresenceOf("age") might exist, addError() gives you extra conditional control.
"},"hint":"Adds a custom error to a model instance. This is useful when built-in validations don’t fully cover your business rules, or when you want to enforce conditional logic. The error will be attached to the given property and can later be retrieved using functions like errorsOn() or allErrors().\n\n","returntype":"void","slug":"model.addError","parameters":[{"required":true,"hint":"The name of the property you want to add an error on.","name":"property","type":"string"},{"required":true,"hint":"The error message (such as \"Please enter a correct name in the form field\" for example).","name":"message","type":"string"},{"default":"","required":false,"hint":"A name to identify the error by (useful when you need to distinguish one error from another one set on the same object and you don't want to use the error message itself for that).","name":"name","type":"string"}],"availableIn":["model"],"name":"addError","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Add a general error\nthis.addErrorToBase(\n    message="Your email address must be the same as your domain name."\n);\n\nError applies to the whole object, not just email.\n\n2. Add a named error\nthis.addErrorToBase(\n    message="Order total must be greater than zero.",\n    name="invalidTotal"\n);\n\nUseful for distinguishing this error later when multiple base errors exist.\n\n3. Enforce a cross-property rule\nif (this.startDate > this.endDate) {\n    this.addErrorToBase(\n        message="Start date cannot be after end date.",\n        name="invalidDateRange"\n    );\n}\n\nRule depends on two properties, so the error belongs on the object as a whole.\n\n4. Business logic validation\nif (this.balance < this.minimumDeposit) {\n    this.addErrorToBase(\n        message="Balance is below the required minimum deposit.",\n        name="lowBalance"\n    );\n}\n\nExample where validation involves external business rules, not just a single column.\n\n5. Using with valid()\nif (!user.valid()) {\n    writeDump(user.allErrors());\n    // Will include base-level errors from addErrorToBase()\n}
"},"hint":"Adds an error directly on the model object itself, not tied to a specific property. This is useful when the error applies to the object as a whole or to a combination of properties, rather than a single field (for example: comparing two values, enforcing cross-property business rules, or validating external conditions).\n\n","returntype":"void","slug":"model.addErrorToBase","parameters":[{"required":true,"hint":"The error message (such as \"Please enter a correct name in the form field\" for example).","name":"message","type":"string"},{"default":"","required":false,"hint":"A name to identify the error by (useful when you need to distinguish one error from another one set on the same object and you don't want to use the error message itself for that).","name":"name","type":"string"}],"availableIn":["model"],"name":"addErrorToBase","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Basic foreign key\naddForeignKey(\n    table="orders",\n    referenceTable="users",\n    column="userId",\n    referenceColumn="id"\n);\n\nEnsures that every orders.userId must exist in users.id.\n\n2. Foreign key for many-to-one relation\naddForeignKey(\n    table="comments",\n    referenceTable="posts",\n    column="postId",\n    referenceColumn="id"\n);\n\nEnsures each comment is linked to a valid post.\n\n3. Foreign key with a custom reference column\naddForeignKey(\n    table="invoices",\n    referenceTable="customers",\n    column="customerCode",\n    referenceColumn="code"\n);\n\nLinks invoices.customerCode to customers.code instead of a numeric ID.\n\n4. Multiple foreign keys in one migration\n// In migration\naddForeignKey(\n    table="enrollments",\n    referenceTable="students",\n    column="studentId",\n    referenceColumn="id"\n);\n\naddForeignKey(\n    table="enrollments",\n    referenceTable="courses",\n    column="courseId",\n    referenceColumn="id"\n);\n\nThe enrollments table is linked to both students and courses.
"},"hint":"Adds a foreign key constraint between two tables. This ensures that values in one table’s column must exist in the referenced column of another table, enforcing referential integrity. This function is only available inside a migration CFC and is part of the Wheels migrator API.\n\n","returntype":"void","slug":"migration.addForeignKey","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"The reference table name to perform the operation on","name":"referenceTable","type":"string"},{"required":true,"hint":"The column name to perform the operation on","name":"column","type":"string"},{"required":true,"hint":"The reference column name to perform the operation on","name":"referenceColumn","type":"string"}],"availableIn":["migration"],"name":"addForeignKey","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a JavaScript format\naddFormat(\n    extension="js",\n    mimeType="text/javascript"\n);\n\nAllows controllers to respond to .js requests with the correct MIME type.\n\n2. Add PowerPoint formats\naddFormat(extension="ppt", mimeType="application/vnd.ms-powerpoint");\naddFormat(extension="pptx", mimeType="application/vnd.ms-powerpoint");\n\nEnables Wheels to correctly serve legacy and modern PowerPoint files.\n\n3. Add JSON format\naddFormat(\n    extension="json",\n    mimeType="application/json"\n);\n\nUseful for APIs that need to respond with .json requests.\n\n4. Add PDF format\naddFormat(\n    extension="pdf",\n    mimeType="application/pdf"\n);\n\nEnsures .pdf responses are correctly labeled for browsers.\n\n5. Add multiple custom data formats\naddFormat(extension="csv", mimeType="text/csv");\naddFormat(extension="yaml", mimeType="application/x-yaml");\n\nExpands your app to handle CSV and YAML outputs.
"},"hint":"Registers a new MIME type in your Wheels application for use with responding to multiple formats. This is helpful when your app needs to handle file types beyond the defaults provided by Wheels (e.g., serving JavaScript, PowerPoint, JSON, custom data formats).\n\n","returntype":"void","slug":"controller.addFormat","parameters":[{"required":true,"hint":"File extension to add.","name":"extension","type":"string"},{"required":true,"hint":"Matching MIME type to associate with the file extension.","name":"mimeType","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"addFormat","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"configuration","category":"Miscellaneous Functions","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a unique index on a single column\naddIndex(\n    table="members",\n    columnNames="username",\n    unique=true\n);\n\nEnsures username values in members are unique.\n\n2. Add a non-unique index for faster queries\naddIndex(\n    table="orders",\n    columnNames="createdAt"\n);\n\nSpeeds up queries filtering or ordering by createdAt.\n\n3. Add a composite index (multiple columns)\naddIndex(\n    table="posts",\n    columnNames="authorId,createdAt"\n);\n\nOptimizes queries that filter or sort on both authorId and createdAt.\n\n4. Add an index with a custom name\naddIndex(\n    table="comments",\n    columnNames="postId",\n    indexName="idx_comments_postId"\n);\n\nCreates index with a custom name instead of default comments_postId.\n\n5. Composite unique index\naddIndex(\n    table="enrollments",\n    columnNames="studentId,courseId",\n    unique=true,\n    indexName="unique_enrollments"\n);\n\nPrevents the same studentId and courseId pair from being inserted more than once.
"},"hint":"Adds a database index on one or more columns of a table. Indexes speed up queries that filter, sort, or join on those columns. This function is only available inside a migration CFC and is part of the Wheels migrator API.\n\n","returntype":"void","slug":"migration.addIndex","parameters":[{"required":true,"hint":"The table name to perform the index operation on","name":"table","type":"string"},{"required":false,"hint":"One or more column names to index, comma separated","name":"columnNames","type":"string"},{"default":"false","required":false,"hint":"If true will create a unique index constraint","name":"unique","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"The name of the index to add: Defaults to table name + underscore + first column name","name":"indexName","type":"string"}],"availableIn":["migration"],"name":"addIndex","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single record\naddRecord(\n    table="people",\n    id=1,\n    title="Mr",\n    firstname="Bruce",\n    lastname="Wayne", \n    email="bruce@wayneenterprises.com",\n    tel="555-67869099"\n);\n\nInserts one record into the people table.\n\n2. Add a record with only required fields\naddRecord(\n    table="roles",\n    id=1,\n    name="Admin"\n);\n\nSeeds an Admin role into the roles table.\n\n3. Add a record with default values in schema\naddRecord(\n    table="users",\n    email="new@example.com",\n    firstName="new",\n    lastName="user"\n);\n\nRelies on schema defaults (e.g., isActive=true) for missing fields.\n\n4. Add lookup data\naddRecord(\n    table="statuses",\n    id=1,\n    name="Pending"\n);\naddRecord(\n    table="statuses",\n    id=2,\n    name="Approved"\n);\naddRecord(\n    table="statuses",\n    id=3,\n    name="Rejected"\n);\n\nSeeds reusable lookup/status values.\n\n5. Add a record referencing another table\n// Assuming user with ID=1 exists\naddRecord(\n    table="posts",\n    id=1,\n    title="First Post",\n    content="Hello, Wheels!",\n    userId=1\n);\n\nCreates a post tied to an existing user.
"},"hint":"Inserts a new record into a table. This function is only available inside a migration CFC and is part of the Wheels migrator API. Useful for seeding initial data (like admin users, roles, or lookup values) alongside schema changes.\n\n","returntype":"void","slug":"migration.addRecord","parameters":[{"required":true,"hint":"The table name to add the record to","name":"table","type":"string"}],"availableIn":["migration"],"name":"addRecord","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a user reference to orders\naddReference(\n    table="orders",\n    referenceName="users"\n);\n\nAdds a userId column to orders and creates a foreign key to users.id.\n\n2. Add a post reference to comments\naddReference(\n    table="comments",\n    referenceName="posts"\n);\n\nCreates a postId column on comments and links it to posts.id.\n\n3. Add references to multiple tables\naddReference(table="enrollments", referenceName="students");\naddReference(table="enrollments", referenceName="courses");\n\nAdds both studentId and courseId to enrollments with foreign keys to students and courses.\n\n4. Composite example (reference + other fields)\naddColumn(table="votes", columnType="boolean", columnName="upvote", default=1);\naddReference(table="votes", referenceName="users");\naddReference(table="votes", referenceName="posts");\n\nBuilds a votes table that connects users and posts with foreign keys.
"},"hint":"Adds a reference column and a foreign key constraint to a table in one step. This is a shortcut for creating an integer column (e.g., userId) and then linking it to another table using a foreign key. This function is only available inside a migration CFC and is part of the Wheels migrator API.\n\n","returntype":"void","slug":"migration.addReference","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"The reference table name to perform the operation on","name":"referenceName","type":"string"}],"availableIn":["migration"],"name":"addReference","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Single callback method\n// Instruct Wheels to call the `fixObj` method after an object is created\nafterCreate(\"fixObj\");\n\nfunction fixObj() {\n    variables.fixed = true;\n}\n\n2. Multiple callbacks\nafterCreate(\"logCreation,notifyAdmin\");\n\nfunction logCreation() {\n    writeLog(\"New record created at #now()#\");\n}\n\nfunction notifyAdmin() {\n    // send an email notification\n}\n\n3. With object attributes\nafterCreate(\"setDefaults\");\n\nfunction setDefaults() {\n    if (!len(variables.status)) {\n        variables.status = \"pending\";\n    }\n}\n\n4. Practical usage in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterCreate(\"assignRole,sendWelcomeEmail\");\n    }\n\n    function assignRole() {\n        if (isNull(roleId)) {\n            roleId = Role.findOneByName(\"User\").id;\n        }\n    }\n\n    function sendWelcomeEmail() {\n        // code to send welcome email\n    }\n}
"},"hint":"Registers one or more callback methods that are automatically executed after a new object is created (i.e., after calling create() on a model). This is part of the model lifecycle callbacks in Wheels.\n\n","returntype":"void","slug":"model.afterCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Single callback method\n// Call `logDeletion` after an object is deleted\nafterDelete(\"logDeletion\");\n\nfunction logDeletion() {\n    writeLog(\"Record deleted at #now()#\");\n}\n\n2. Multiple callbacks\nafterDelete(\"archiveData,notifyAdmin\");\n\nfunction archiveData() {\n    // move deleted data to an archive table\n}\n\nfunction notifyAdmin() {\n    // send a notification email\n}\n\n3. With related cleanup\nafterDelete(\"removeAssociatedRecords\");\n\nfunction removeAssociatedRecords() {\n    // remove orphaned child records manually\n    Order.deleteAll(where=\"userId = #this.id#\");\n}\n\n4. Practical usage in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterDelete(\"cleanupSessions,sendGoodbyeEmail\");\n    }\n\n    function cleanupSessions() {\n        Session.deleteAll(where=\"userId = #id#\");\n    }\n\n    function sendGoodbyeEmail() {\n        // code to send a farewell email\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object is deleted from the database. This hook allows you to perform cleanup, logging, or side effects when a record has been removed.\n\n","returntype":"void","slug":"model.afterDelete","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterDelete","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a timestamp when data was fetched\ncomponent extends=\"Model\" {\n    function config() {\n        afterFind(\"setTime\");\n    }\n\n    function setTime() {\n        arguments.fetchedAt = now();\n        return arguments;\n    }\n}\n\nWhen you call:\n\nuser = model(\"User\").findByKey(1);\nwriteOutput(user.fetchedAt); // Shows the time record was retrieved\n\n2. Format or normalize data\nafterFind(\"normalizeEmail\");\n\nfunction normalizeEmail() {\n    this.email = lcase(this.email);\n}\n\nEnsures all email addresses are lowercased when loaded.\n\n3. Load related info automatically\nafterFind(\"attachProfile\");\n\nfunction attachProfile() {\n    this.profile = model(\"Profile\").findOne(where=\"userId = #this.id#\");\n}\n\nNow every User object automatically has its related profile loaded.\n\n4. Multiple callbacks\nafterFind(\"setTime,normalizeEmail,attachProfile\");\n\nAll three methods will run in order after the object is retrieved.
"},"hint":"Registers one or more callback methods that should be executed after an existing object has been initialized, typically via finder methods such as findByKey, findOne, findAll, or other query-based lookups. This hook is useful for adjusting, enriching, or transforming model objects immediately after they are loaded from the database.\n\n","returntype":"void","slug":"model.afterFind","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterFind","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Normalize data after every initialization\nafterInitialization(\"normalizeName\");\n\nfunction normalizeName() {\n    this.firstName = trim(this.firstName);\n    this.lastName = trim(this.lastName);\n}\n\nEnsures whitespace is stripped whether the object is new or fetched.\n\n2. Add a helper attribute for all instances\nafterInitialization(\"addFullName\");\n\nfunction addFullName() {\n    this.fullName = this.firstName & \" \" & this.lastName;\n}\n\nNow every object has a fullName property set right after creation or retrieval.\n\n3. Multiple callbacks\nafterInitialization(\"normalizeName,addFullName\");\n\nRuns both methods sequentially.\n\n4. Practical example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterInitialization(\"normalizeName,addFullName,setFetchedAt\");\n    }\n\n    function normalizeName() {\n        this.firstName = trim(this.firstName);\n        this.lastName = trim(this.lastName);\n    }\n\n    function addFullName() {\n        this.fullName = this.firstName & \" \" & this.lastName;\n    }\n\n    function setFetchedAt() {\n        arguments.fetchedAt = now();\n        return arguments;\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object has been initialized. Initialization happens in two cases, When a new object is created (via new() or similar) or when an existing object is fetched from the database (via findByKey, findOne, etc.). This makes afterInitialization() more general than afterCreate() or afterFind(), since it runs in both scenarios.\n\n","returntype":"void","slug":"model.afterInitialization","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterInitialization","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Set default values for new records\nafterNew(\"setDefaults\");\n\nfunction setDefaults() {\n    this.isActive = true;\n    this.role = \"member\";\n}\n\nWhenever a new object is initialized, default values are assigned.\n\n2. Generate a temporary property\nafterNew(\"assignTempId\");\n\nfunction assignTempId() {\n    this.tempId = createUUID();\n}\n\nEach new object will have a unique tempId until it’s saved.\n\n3. Multiple callbacks\nafterNew(\"setDefaults,assignTempId\");\n\nRuns both methods sequentially for every new object.\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterNew(\"setDefaults,prepareDisplayName\");\n    }\n\n    function setDefaults() {\n        this.isActive = true;\n    }\n\n    function prepareDisplayName() {\n        this.displayName = this.firstName & \" \" & this.lastName;\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after a new object has been initialized, typically via the new() method. This hook is useful for setting default values, preparing derived attributes, or running logic every time you create a fresh model instance (before saving it to the database).\n\n","returntype":"void","slug":"model.afterNew","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterNew","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Log every save\nafterSave(\"logSave\");\n\nfunction logSave() {\n    writeLog(\"User ##this.id## saved at #now()#\");\n}\n\n2. Trigger notifications\nafterSave(\"notifyAdmin\");\n\nfunction notifyAdmin() {\n    if (this.role == \"admin\") {\n        sendEmail(to=\"superadmin@example.com\", subject=\"Admin Updated\", body=\"Admin user #this.id# has been updated.\");\n    }\n}\n\n3. Multiple callbacks\nafterSave(\"logSave,notifyAdmin\");\n\n4. Example in Order.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterSave(\"recalculateInventory,sendConfirmation\");\n    }\n\n    function recalculateInventory() {\n        Inventory.updateStock(this.productId, -this.quantity);\n    }\n\n    function sendConfirmation() {\n        EmailService.sendOrderConfirmation(this.id);\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object is saved to the database. This hook runs whether the save was the result of creating a new record or updating an existing one. It’s ideal for tasks that must happen after persistence, such as logging, syncing data, or triggering external processes.\n\n","returntype":"void","slug":"model.afterSave","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterSave","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Simple logging\nafterUpdate(\"logUpdate\");\n\nfunction logUpdate() {\n    writeLog(\"Record ##this.id## was updated at #now()#\");\n}\n\n2. Trigger an email when a specific field changes\nafterUpdate(\"notifyEmailChange\");\n\nfunction notifyEmailChange() {\n    if (this.hasChanged(\"email\")) {\n        sendEmail(\n            to=this.email,\n            subject=\"Your email was updated\",\n            body=\"Hi #this.firstName#, your email address has been changed.\"\n        );\n    }\n}\n\n3. Multiple callbacks\nafterUpdate(\"logUpdate,notifyEmailChange\");\n\n4. Example in Order.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        afterUpdate(\"updateInventory,sendUpdateNotification\");\n    }\n\n    function updateInventory() {\n        Inventory.adjustStock(this.productId, -this.quantity);\n    }\n\n    function sendUpdateNotification() {\n        EmailService.sendOrderUpdate(this.id);\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an existing object has been updated in the database. This hook is ideal for performing follow-up tasks whenever a record changes — such as logging, cache invalidation, or sending notifications about updates.\n\n","returntype":"void","slug":"model.afterUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a custom validation error\nafterValidation(\"checkRestrictedEmails\");\n\nfunction checkRestrictedEmails() {\n    if (listFindNoCase(\"test@example.com,admin@example.com\", this.email)) {\n        this.addError(\"email\", \"That email address is not allowed.\");\n    }\n}\n\n2. Normalize data after validation\nafterValidation(\"normalizePhone\");\n\nfunction normalizePhone() {\n    if (len(this.phone)) {\n        this.phone = rereplace(this.phone, \"[^0-9]\", \"\", \"all\");\n    }\n}\n\n3. Multiple callbacks\nafterValidation(\"checkRestrictedEmails,normalizePhone\");\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        validatesPresenceOf(\"email\");\n        afterValidation(\"checkRestrictedEmails,normalizePhone\");\n    }\n\n    function checkRestrictedEmails() {\n        if (listFindNoCase(\"banned@example.com\", this.email)) {\n            this.addError(\"email\", \"This email address is not permitted.\");\n        }\n    }\n\n    function normalizePhone() {\n        this.phone = rereplace(this.phone, \"[^0-9]\", \"\", \"all\");\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an object has been validated. This hook is useful for running extra logic that depends on validation results, such as adjusting error messages, performing side validations, or preparing data before saving.\n\n","returntype":"void","slug":"model.afterValidation","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterValidation","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a creation-only error\nafterValidationOnCreate(\"checkSignupEmail\");\n\nfunction checkSignupEmail() {\n    if (listFindNoCase(\"banned@example.com,blocked@example.com\", this.email)) {\n        this.addError(\"email\", \"This email address cannot be used for registration.\");\n    }\n}\n\n2. Generate a default username if missing\nafterValidationOnCreate(\"generateUsername\");\n\nfunction generateUsername() {\n    if (!len(this.username)) {\n        this.username = listFirst(this.email, \"@\");\n    }\n}\n\n3. Multiple callbacks\nafterValidationOnCreate(\"checkSignupEmail,generateUsername\");\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        validatesPresenceOf(\"email\");\n        validatesFormatOf(property=\"email\", regex=\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\");\n\n        afterValidationOnCreate(\"checkSignupEmail,generateUsername\");\n    }\n\n    function checkSignupEmail() {\n        if (listFindNoCase(\"banned@example.com\", this.email)) {\n            this.addError(\"email\", \"This email address is restricted.\");\n        }\n    }\n\n    function generateUsername() {\n        if (!len(this.username)) {\n            this.username = listFirst(this.email, \"@\");\n        }\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after a new object has been validated (i.e., when running validations during a create() or save() on a new record). This hook is useful when you want to apply custom logic only during new record creation, not during updates.\n\n","returntype":"void","slug":"model.afterValidationOnCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterValidationOnCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Prevent updating restricted emails\nafterValidationOnUpdate(\"checkRestrictedEmail\");\n\nfunction checkRestrictedEmail() {\n    if (this.email eq \"admin@example.com\") {\n        this.addError(\"email\", \"You cannot change this email address.\");\n    }\n}\n\n2. Automatically update a lastModifiedBy field\nafterValidationOnUpdate(\"setLastModifiedBy\");\n\nfunction setLastModifiedBy() {\n    this.lastModifiedBy = session.userId;\n}\n\n3. Multiple callbacks\nafterValidationOnUpdate(\"checkRestrictedEmail,setLastModifiedBy\");\n\n4. Example in User.cfc\ncomponent extends=\"Model\" {\n    function config() {\n        validatesPresenceOf(\"email\");\n        validatesFormatOf(property=\"email\", regex=\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\");\n\n        afterValidationOnUpdate(\"checkRestrictedEmail,setLastModifiedBy\");\n    }\n\n    function checkRestrictedEmail() {\n        if (this.email eq \"admin@example.com\") {\n            this.addError(\"email\", \"This email cannot be changed.\");\n        }\n    }\n\n    function setLastModifiedBy() {\n        this.lastModifiedBy = session.userId;\n    }\n}
"},"hint":"Registers one or more callback methods that should be executed after an existing object has been validated (i.e., when running validations during an update() or save() on an already-persisted record). This hook is useful when you want logic to run only on updates, not on initial creation.\n\n","returntype":"void","slug":"model.afterValidationOnUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"afterValidationOnUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\nmember = model(\"member\").findByKey(params.memberId);\n\n// Change some values (not saved yet)\nmember.firstName = params.newFirstName;\nmember.email = params.newEmail;\n\n// Get all pending changes\nallChanges = member.allChanges();\n// Example output: {\"email\":{\"CHANGEDTO\":\"old@gmail.com\",\"CHANGEDFROM\":\"new@gmail.com\"},\"firstname\":{\"CHANGEDTO\":\"old\",\"CHANGEDFROM\":\"new\"}}\n\n2. Checking if changes exist before saving\nmember = model(\"member\").findByKey(42);\nmember.status = \"inactive\";\n\nif (!structIsEmpty(member.allChanges())) {\n    writeDump(var=member.allChanges(), label=\"Pending Changes\");\n    member.save();\n}\n\n3. Using in a validation callback\nafterValidation(\"logChanges\");\n\nfunction logChanges() {\n    var changes = this.allChanges();\n    if (!structIsEmpty(changes)) {\n        log(message=\"User ##this.id## updated fields: #structKeyList(changes)#\");\n    }\n}\n\n4. Example with multiple updates\nuser = model(\"user\").findByKey(10);\n\nuser.firstName = \"Jane\";\nuser.lastName  = \"Doe\";\nuser.email     = \"jane.doe@example.com\";\n\nchanges = user.allChanges();\n// Output might be: {\"email\":{\"CHANGEDTO\":\"jane.doe@example.com\",\"CHANGEDFROM\":\"example.user@gmail.com\"},\"lastname\":{\"CHANGEDTO\":\"Doe\",\"CHANGEDFROM\":\"user\"},\"firstname\":{\"CHANGEDTO\":\"Jane\",\"CHANGEDFROM\":\"example\"}}
"},"hint":"Returns a struct containing all unsaved changes made to an object since it was last loaded or saved. Each entry in the struct uses the property name as the key and the new (unsaved) value as the value.\n\n","returntype":"struct","slug":"model.allChanges","parameters":[],"availableIn":["model"],"name":"allChanges","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Get all validation errors\nuser = model(\"user\").new(\n    username = \"\",\n    password = \"\"\n);\n\n// Validate the object\nuser.valid();\n\n// Fetch errors\nerrorInfo = user.allErrors();\n\nwriteDump(var=errorInfo, label=\"User Errors\");\n\nSample output:\n\n[\n  {\n    \"message\": \"Username must not be blank.\",\n    \"name\": \"PresenceOf\",\n    \"property\": \"username\"\n  },\n  {\n    \"message\": \"Password must not be blank.\",\n    \"name\": \"PresenceOf\",\n    \"property\": \"password\"\n  }\n]\n\n2. Including associated model errors\norder = model(\"order\").new(\n    customer = model(\"customer\").new(name=\"\")\n);\n\n// Validate both order and associated customer\norder.valid();\n\n// Get errors from both order and customer\nerrors = order.allErrors(includeAssociations=true);\n\n3. Checking for errors before saving\nuser = model(\"user\").new(email=\"not-an-email\");\n\nif (!user.valid()) {\n    errors = user.allErrors();\n    for (err in errors) {\n        writeOutput(\"Error on #err.property#: #err.message#\");\n    }\n}
"},"hint":"Returns an array of all the errors on the object.\n\n\nIt does this by storing instances of models that are associations, and not checking associations of those instances because they have already been checked.","returntype":"array","slug":"model.allErrors","parameters":[{"default":false,"required":false,"name":"includeAssociations","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"is a private argument not meant to be used by the user, the function uses this to ensure circular dependency avoidance.","name":"seenErrors","type":"array"}],"availableIn":["model"],"name":"allErrors","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Announce a step in a migration\nannounce(\"Adding status column to members table...\");\naddColumn(\n    table = \"members\",\n    columnType = \"string\",\n    columnName = \"status\",\n    limit = 50\n);\n\n2. Announce progress in multiple steps\nannounce(\"Creating orders table...\");\ncreateTable(\"orders\", function(table) {\n    table.integer(\"id\");\n    table.string(\"description\");\n});\n\nannounce(\"Adding index on orders.description...\");\naddIndex(table=\"orders\", columnNames=\"description\");\n\n3. Use for debugging migrations\nannounce(\"Starting migration at #Now()#\");\n\n// Migration logic here...\n\nannounce(\"Migration completed successfully.\");
"},"hint":"Outputs a custom message during migration execution. This is useful for logging progress or providing context when multiple migration steps are running.\n\n","returntype":"any","slug":"migration.announce","parameters":[{"required":true,"name":"message","type":"string"}],"availableIn":["migration","tabledefinition"],"name":"announce","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Basic true assertion\n// Passes because 2 + 2 = 4\nassert(\"2 + 2 EQ 4\");\n\n2. Assertion that fails\n// This will fail the test because 5 is not less than 3\nassert(\"5 LT 3\");\n\n3. With model object conditions\nuser = model(\"user\").findByKey(1);\n\n// Assert that the user has an email set\nassert(len(user.email));
"},"hint":"Asserts that an expression evaluates to true in a test. If the expression evaluates to false, the test will fail and an error will be raised. This is one of the core testing functions available when writing legacy tests in Wheels.","returntype":"void","slug":"test.assert","parameters":[{"required":true,"name":"expression","type":"string"}],"availableIn":["test"],"name":"assert","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Get the raw CSRF token in a controller\ntoken = authenticityToken();\n\n2. Output token manually in a form (not recommended, but possible)\n<form action=\"/posts/create\" method=\"post\">\n    <input type=\"hidden\" name=\"authenticityToken\" value=\"#authenticityToken()#\">\n    <input type=\"text\" name=\"title\">\n    <input type=\"submit\" value=\"Save\">\n</form>\n\n3. Use in AJAX request headers\nfetch(\"/posts/create\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    \"X-CSRF-Token\": \"#authenticityToken()#\"\n  },\n  body: JSON.stringify({ title: \"New Post\" })\n});
"},"hint":"Returns the raw CSRF authenticity token for the current user session. This token is used to help protect against Cross-Site Request Forgery (CSRF) attacks by verifying that form submissions or AJAX requests originate from your application. You typically won’t call this function directly in views — instead, Wheels provides helpers like authenticityTokenField() to generate hidden form fields. But authenticityToken() can be useful if you need direct access to the token string (for example, in custom JavaScript code).\n\n","returntype":"string","slug":"controller.authenticityToken","parameters":[],"availableIn":["controller"],"name":"authenticityToken","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Adding a CSRF token to a manual form\n<!--- Needed here because we're not using startFormTag --->\n<form action=\"#urlFor(route='posts')#\" method=\"post\">\n  #authenticityTokenField()#\n  <input type=\"text\" name=\"title\">\n  <input type=\"submit\" value=\"Create Post\">\n</form>\n\n2. No token needed for safe GET forms\n<!--- Not needed here because GET requests are not protected --->\n<form action=\"#urlFor(route='invoices')#\" method=\"get\">\n  <input type=\"text\" name=\"search\">\n  <input type=\"submit\" value=\"Find Invoice\">\n</form>\n\n3. Custom AJAX form with CSRF token\n<form id=\"ajaxForm\">\n  #authenticityTokenField()#\n  <input type=\"text\" name=\"title\">\n  <button type=\"submit\">Save</button>\n</form>\n\ndocument.getElementById(\"ajaxForm\").addEventListener(\"submit\", function(e) {\n  e.preventDefault();\n\n  const token = document.querySelector(\"input[name='authenticityToken']\").value;\n\n  fetch(\"/posts\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      \"X-CSRF-Token\": token\n    },\n    body: JSON.stringify({ title: \"CSRF-protected post\" })\n  });\n});
"},"hint":"Generates a hidden form field that contains a CSRF authenticity token. This token is required for verifying that POST, PUT, PATCH, or DELETE requests originated from your application, helping protect against Cross-Site Request Forgery (CSRF) attacks. When you use startFormTag(), Wheels automatically includes the token field for you. You’ll usually only need to call authenticityTokenField() manually when creating forms without startFormTag() or when building raw HTML forms.\n\n","returntype":"string","slug":"controller.authenticityTokenField","parameters":[],"availableIn":["controller"],"name":"authenticityTokenField","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Auto-link a URL\n#autoLink(\"Download Wheels from https://wheels.dev\")#\n\nOutput:\n\nDownload Wheels from <a href=\"https://wheels.dev\">https://wheels.dev</a>\n\n2. Auto-link an email address\n#autoLink(\"Email us at info@cfwheels.org\")#\n\nOutput:\n\nEmail us at <a href=\"mailto:info@cfwheels.org\">info@cfwheels.org</a>\n\n3. Only link URLs, not emails\n#autoLink(text=\"Visit https://cfwheels.org or email support@cfwheels.org\", link=\"URLs\")#\n\nOutput:\n\nVisit <a href=\"https://cfwheels.org\">https://cfwheels.org</a> or email support@cfwheels.org\n\n4. Only link email addresses\n#autoLink(text=\"Contact info@cfwheels.org or see https://cfwheels.org\", link=\"emailAddresses\")#\n\nOutput:\n\nContact <a href=\"mailto:info@cfwheels.org\">info@cfwheels.org</a> or see https://cfwheels.org\n\n5. Disable auto-linking of relative URLs\n#autoLink(text=\"See /about for more info\", relative=false)#\n\nOutput:\n\nSee /about for more info
"},"hint":"Scans a block of text for URLs and/or email addresses and automatically converts them into clickable links. This helper is handy for displaying user-generated content, comments, or messages where you want to make links interactive without manually adding
tags.\n\n","returntype":"string","slug":"controller.autoLink","parameters":[{"required":true,"hint":"The text to create links in.","name":"text","type":"string"},{"default":"all","required":false,"hint":"Whether to link URLs, email addresses or both. Possible values are: `all` (default), `URLs` and `emailAddresses`.","name":"link","type":"string"},{"default":true,"required":false,"hint":"Should we auto-link relative urls.","name":"relative","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"autoLink","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Disable automatic validations for a single model\ncomponent extends=\"Model\" {\n    function config() {\n        automaticValidations(false);\n    }\n}\n\n\nUseful when automatic validations are enabled globally but a model requires custom validation handling.\n\n2. Enable automatic validations explicitly for a model\ncomponent extends=\"Model\" {\n    function config() {\n        automaticValidations(true);\n    }\n}\n\n\nEnsures this model always applies database-inferred validations, even if global automatic validations are turned off.\n\n3. Combining with custom validations\ncomponent extends=\"Model\" {\n    function config() {\n        automaticValidations(false); // turn off inferred rules\n        validatesPresenceOf(\"email\");\n        validatesFormatOf(property=\"email\", regex=\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\");\n    }\n}\n\n\nHere, automatic validations are disabled, but explicit validation rules are still applied.
"},"hint":"Controls whether automatic validations should be enabled for a specific model. By default, Wheels can automatically infer validations from your database schema (e.g., NOT NULL fields, field length limits, etc.). This function lets you override that behavior at the model level — enabling or disabling automatic validations regardless of the global setting.\n\n","returntype":"void","slug":"model.automaticValidations","parameters":[{"required":true,"hint":"Set to `true` or `false`.","name":"value","type":"boolean"}],"availableIn":["model"],"name":"automaticValidations","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Average salary for all employees\navgSalary = model(\"employee\").average(\"salary\");\n\n2. Average salary filtered by department\navgSalary = model(\"employee\").average(\n    property = \"salary\",\n    where    = \"departmentId = #params.key#\"\n);\n\n3. Ensure a numeric value is always returned\navgSalary = model(\"employee\").average(\n    property = \"salary\",\n    where    = \"salary BETWEEN #params.min# AND #params.max#\",\n    ifNull   = 0\n);\n\n4. Average with distinct values only\navgSalary = model(\"employee\").average(\n    property = \"salary\",\n    distinct = true\n);\n\n5. Grouped average by department\navgSalaries = model(\"employee\").average(\n    property = \"salary\",\n    group    = \"departmentId\"\n);
"},"hint":"Calculates the average value for a given property.\nUses the SQL function AVG.\nIf no records can be found to perform the calculation on you can use the ifNull argument to decide what should be returned.\n\n","returntype":"any","slug":"model.average","parameters":[{"required":true,"hint":"Name of the property to calculate the average for.","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":false,"required":false,"hint":"When `true`, `AVG` will be performed only on each unique instance of a value, regardless of how many times the value occurs.","name":"distinct","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"average","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Run a method before saving a new object\nfunction config() {\n    beforeCreate(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Ensure a default role is assigned\n    if (!structKeyExists(this, \"roleId\")) {\n        this.roleId = 2; // Assign \"user\" role\n    }\n}\n\n2. Generate a unique slug before creation\nfunction config() {\n    beforeCreate(\"generateSlug\");\n}\n\nfunction generateSlug() {\n    this.slug = lcase(replace(this.title, \" \", \"-\", \"all\"));\n}\n\n3. Hash a password before inserting a new user\nfunction config() {\n    beforeCreate(\"hashPassword\");\n}\n\nfunction hashPassword() {\n    this.password = hash(this.password, \"SHA-256\");\n}
"},"hint":"Registers method(s) that should be called before a new object is created. This allows you to modify or validate data, set defaults, or perform logic right before the object is persisted in the database for the first time.\n\n","returntype":"void","slug":"model.beforeCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: run a method before deleting\nfunction config() {\n    beforeDelete(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: log deletions\n    writeLog(\"Deleting record with ID #this.id#\");\n}\n\n2. Prevent deletion if conditions fail\nfunction config() {\n    beforeDelete(\"checkIfAdmin\");\n}\n\nfunction checkIfAdmin() {\n    if (!session.isAdmin) {\n        throw(type=\"SecurityException\", message=\"Only admins can delete records.\");\n    }\n}\n\n3. Cascade cleanup before deletion\nfunction config() {\n    beforeDelete(\"cleanupAssociations\");\n}\n\nfunction cleanupAssociations() {\n    // Delete related comments before removing a post\n    model(\"comment\").deleteAll(where=\"postId = #this.id#\");\n}
"},"hint":"Registers method(s) that should be called before an object is deleted. This allows you to perform cleanup, enforce constraints, or prevent deletion if certain conditions are not met.\n\n","returntype":"void","slug":"model.beforeDelete","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeDelete","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: run a method before save\nfunction config() {\n    beforeSave(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: Trim whitespace before saving\n    this.username = trim(this.username);\n}\n\n2. Automatically update a timestamp\nfunction config() {\n    beforeSave(\"updateTimestamp\");\n}\n\nfunction updateTimestamp() {\n    this.lastModifiedAt = now();\n}\n\n3. Normalize data before saving\nfunction config() {\n    beforeSave(\"normalizeData\");\n}\n\nfunction normalizeData() {\n    // Example: ensure email is lowercase\n    this.email = lcase(this.email);\n\n    // Example: capitalize first name\n    this.firstName = ucase(left(this.firstName, 1)) & mid(this.firstName, 2);\n}\n\n4. Prevent save if conditions fail\nfunction config() {\n    beforeSave(\"blockInactiveUsers\");\n}\n\nfunction blockInactiveUsers() {\n    if (!this.isActive) {\n        throw(type=\"ValidationException\", message=\"Inactive users cannot be saved.\");\n    }\n}
"},"hint":"Registers method(s) that should be called before an object is saved. This is useful for performing transformations, validations, or logging before data is persisted.\n\n","returntype":"void","slug":"model.beforeSave","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeSave","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before update\nfunction config() {\n    beforeUpdate(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: trim whitespace before updating\n    this.lastName = trim(this.lastName);\n}\n\n2. Update an \\\"last modified\\\" timestamp\nfunction config() {\n    beforeUpdate(\"updateTimestamp\");\n}\n\nfunction updateTimestamp() {\n    this.updatedAt = now();\n}\n\n3. Prevent updating sensitive fields\nfunction config() {\n    beforeUpdate(\"restrictEmailChange\");\n}\n\nfunction restrictEmailChange() {\n    if (this.hasChanged(\"email\")) {\n        throw(type=\"ValidationException\", message=\"Email address cannot be changed.\");\n    }\n}\n\n4. Audit updates with logging\nfunction config() {\n    beforeUpdate(\"logChanges\");\n}\n\nfunction logChanges() {\n    var changes = this.allChanges();\n    writeLog(text=\"User ##this.id## updated with changes: #serializeJSON(changes)#\", file=\"audit\");\n}
"},"hint":"Registers method(s) that should be called before an existing object is updated. This is useful for enforcing rules, transforming values, or checking conditions specifically for update operations (unlike beforeSave(), which applies to both create and update).\n\n","returntype":"void","slug":"model.beforeUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before validation\nfunction config() {\n    beforeValidation(\"fixObj\");\n}\n\nfunction fixObj() {\n    // Example: normalize names before validation\n    this.firstName = trim(this.firstName);\n    this.lastName = trim(this.lastName);\n}\n\n2. Ensure default values before validation\nfunction config() {\n    beforeValidation(\"setDefaults\");\n}\n\nfunction setDefaults() {\n    if (!len(this.status)) {\n        this.status = \"pending\";\n    }\n}\n\n3. Convert input formats before validating\nfunction config() {\n    beforeValidation(\"normalizePhone\");\n}\n\nfunction normalizePhone() {\n    // Remove spaces/dashes so the validation regex can run correctly\n    this.phoneNumber = rereplace(this.phoneNumber, \"[^0-9]\", \"\", \"all\");\n}\n\n4. Multi-method callback\nfunction config() {\n    beforeValidation(\"sanitizeEmail, normalizeUsername\");\n}\n\nfunction sanitizeEmail() {\n    this.email = lcase(trim(this.email));\n}\n\nfunction normalizeUsername() {\n    this.username = rereplace(this.username, \"[^a-zA-Z0-9]\", \"\", \"all\");\n}
"},"hint":"Registers method(s) that should be called before an object is validated. This hook is helpful when you want to adjust, normalize, or clean up data before validation rules run. It ensures the object is in the correct state so that validations pass or fail as expected.\n\n","returntype":"void","slug":"model.beforeValidation","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeValidation","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before validation on create\nfunction config() {\n    beforeValidationOnCreate(\"fixObj\");\n}\n\nfunction fixObj() {\n    this.firstName = trim(this.firstName);\n}\n\n2. Ensure default values only for new records\nfunction config() {\n    beforeValidationOnCreate(\"setDefaults\");\n}\n\nfunction setDefaults() {\n    if (!len(this.role)) {\n        this.role = \"member\";\n    }\n}\n\n3. Normalize data formats for new users\nfunction config() {\n    beforeValidationOnCreate(\"normalizeNewUserData\");\n}\n\nfunction normalizeNewUserData() {\n    // Make sure emails are stored lowercase for new accounts\n    this.email = lcase(trim(this.email));\n}\n\n4. Run multiple setup methods before new record validation\nfunction config() {\n    beforeValidationOnCreate(\"assignUUID, sanitizeName\");\n}\n\nfunction assignUUID() {\n    if (!len(this.uuid)) {\n        this.uuid = createUUID();\n    }\n}\n\nfunction sanitizeName() {\n    this.fullName = trim(this.fullName);\n}
"},"hint":"Registers method(s) that should be called before a new object is validated. This hook is useful when you want to prepare or sanitize data specifically for new records, ensuring that validations run on properly formatted data. It will not run on updates—only on create() or new() + save() operations.\n\n","returntype":"void","slug":"model.beforeValidationOnCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeValidationOnCreate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: register a method before validation on update\nfunction config() {\n    beforeValidationOnUpdate(\"fixObj\");\n}\n\nfunction fixObj() {\n    this.lastName = trim(this.lastName);\n}\n\n2. Prevent changes to immutable fields\nfunction config() {\n    beforeValidationOnUpdate(\"restrictImmutableFields\");\n}\n\nfunction restrictImmutableFields() {\n    if (this.hasChanged(\"email\")) {\n        this.addError(property=\"email\", message=\"Email cannot be changed once set.\");\n    }\n}\n\n3. Normalize input before update validations\nfunction config() {\n    beforeValidationOnUpdate(\"sanitizePhone\");\n}\n\nfunction sanitizePhone() {\n    this.phoneNumber = rereplace(this.phoneNumber, \"[^0-9]\", \"\", \"all\");\n}\n\n4. Run multiple pre-validation methods for updates\nfunction config() {\n    beforeValidationOnUpdate(\"updateTimestamp, sanitizeNotes\");\n}\n\nfunction updateTimestamp() {\n    this.lastModified = now();\n}\n\nfunction sanitizeNotes() {\n    this.notes = trim(this.notes);\n}
"},"hint":"Registers method(s) that should be called before an existing object is validated. This hook is useful when you want to adjust, sanitize, or enforce rules specifically for updates (not for new records). It ensures the object is in the correct state before validation checks run.\n\n","returntype":"void","slug":"model.beforeValidationOnUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names that should be called when this callback event occurs in an object's life cycle (can also be called with the `method` argument).","name":"methods","type":"string"}],"availableIn":["model"],"name":"beforeValidationOnUpdate","tags":{"categoryClass":"callbackfunctions","sectionClass":"modelconfiguration","category":"Callback Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Standard belongsTo association\n// Specify that instances of this model belong to an author\nbelongsTo(\"author\");\n\nWheels will automatically deduce the foreign key as authorId and the associated model as Author.\n\n2. Custom foreign key and model name\n// Foreign key does not follow convention\nbelongsTo(name = \"bookWriter\", modelName = \"author\", foreignKey = \"authorId\");\n\nUseful when your database column names or model names deviate from Wheels conventions.\n\n3. Specify LEFT OUTER JOIN\nbelongsTo(name = \"publisher\", joinType = \"outer\");
"},"hint":"Sets up a belongsTo association between this model and another model. Use this when the current model contains a foreign key referencing another model. This establishes a one-to-many relationship from the perspective of the other model (i.e., this model “belongs to” a parent model).\n\n","returntype":"void","slug":"model.belongsTo","parameters":[{"required":true,"hint":"Gives the association a name that you refer to when working with the association (in the `include` argument to `findAll`, to name one example).","name":"name","type":"string"},{"default":"","required":false,"hint":"Name of associated model (usually not needed if you follow Wheels conventions because the model name will be deduced from the `name` argument).","name":"modelName","type":"string"},{"default":"","required":false,"hint":"Foreign key property name (usually not needed if you follow Wheels conventions since the foreign key name will be deduced from the `name` argument).","name":"foreignKey","type":"string"},{"default":"","required":false,"hint":"Column name to join to if not the primary key (usually not needed if you follow Wheels conventions since the join key will be the table's primary key/keys).","name":"joinKey","type":"string"},{"default":"inner","required":false,"hint":"Use to set the join type when joining associated tables. Possible values are `inner` (for `INNER JOIN`) and `outer` (for `LEFT OUTER JOIN`).","name":"joinType","type":"string"}],"availableIn":["model"],"name":"belongsTo","tags":{"categoryClass":"associationfunctions","sectionClass":"modelconfiguration","category":"Association Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single big integer column\nbigInteger(columnNames=\"userId\");\n\n2. Add multiple big integer columns\nbigInteger(columnNames=\"orderId, invoiceId\");\n\n3. Add a column with a default value and disallow NULLs\nbigInteger(columnNames=\"views\", default=\"0\", allowNull=false);\n\n4. Add a column with a custom limit\nbigInteger(columnNames=\"serialNumber\", limit=20);
"},"hint":"Adds one or more big integer columns to a table definition in a migration. Use this when you need columns capable of storing large integer values, typically larger than standard integer columns.\n\n","returntype":"any","slug":"tabledefinition.bigInteger","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"numeric"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"bigInteger","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single binary column\nbinary(columnNames=\"profilePicture\");\n\n2. Add multiple binary columns\nbinary(columnNames=\"thumbnail, documentBlob\");\n\n3. Add a binary column that allows NULLs\nbinary(columnNames=\"attachment\", allowNull=true);\n\n4. Add a binary column with a default value\nbinary(columnNames=\"signature\", default=\"0x00\");
"},"hint":"Adds one or more binary columns to a table definition in a migration. Use this for storing raw binary data, such as files, images, or other byte streams.\n\n","returntype":"any","slug":"tabledefinition.binary","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"binary","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single boolean column\nboolean(columnNames=\"isActive\");\n\n2. Add multiple boolean columns\nboolean(columnNames=\"isPublished, isVerified\");\n\n3. Add a boolean column with a default value\nboolean(columnNames=\"isAdmin\", default=\"false\");\n\n4. Add a boolean column that allows NULLs\nboolean(columnNames=\"isArchived\", allowNull=true);
"},"hint":"Adds one or more boolean columns to a table definition in a migration. Use this for columns that store true/false values.\n\n","returntype":"any","slug":"tabledefinition.boolean","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"boolean","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic submit button\n#startFormTag(action=\"something\")#\n    #buttonTag(content=\"Submit this form\", value=\"save\")#\n#endFormTag()#\n\n2. Button with a different type\n#buttonTag(content=\"Reset form\", type=\"reset\")#\n\n3. Button using an image\n#buttonTag(image=\"submit.png\", value=\"save\")#\n\n4. Button with HTML wrappers\n#buttonTag(content=\"Click Me\", prepend=\"<div class='btn-wrapper'>\", append=\"</div>\")#\n\n5. Disable encoding for raw HTML content\n#buttonTag(content=\"<strong>Submit</strong>\", encode=false)#
"},"hint":"Builds and returns a string containing a button form control for use in your HTML forms. Use this helper to create buttons with custom content, types, values, images, and optional HTML wrappers.\n\n","returntype":"string","slug":"controller.buttonTag","parameters":[{"default":"Save changes","required":false,"hint":"Content to display inside the button.","name":"content","type":"string"},{"default":"submit","required":false,"hint":"The type for the button: `button`, `reset`, or `submit`.","name":"type","type":"string"},{"default":"save","required":false,"hint":"The value of the button when submitted.","name":"value","type":"string"},{"default":"","required":false,"hint":"File name of the image file to use in the button form control.","name":"image","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"buttonTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic button submitting to an action\n#buttonTo(text=\"Delete Account\", action=\"performDelete\", disable=\"Wait...\")#\n\n2. Button with an ID and class applied to the input\n#buttonTo(text=\"Edit\", action=\"edit\", inputId=\"edit-button\", inputClass=\"edit-button-class\")#\n\n3. Button using an image instead of text\n#buttonTo(image=\"delete-icon.png\", action=\"delete\")#\n\n4. Button linking to a specific route with query parameters\n#buttonTo(text=\"View Report\", route=\"reportRoute\", params=\"year=2025&month=9\")#\n\n5. Button using DELETE method\n#buttonTo(text=\"Remove\", action=\"deleteItem\", method=\"delete\")#
"},"hint":"Creates a form containing a single button that submits to a URL. The URL is constructed the same way as linkTo(). This helper is useful when you want a button that performs a specific action (GET, POST, PUT, DELETE, PATCH) without manually creating a form.\n\n","returntype":"string","slug":"controller.buttonTo","parameters":[{"default":"","required":false,"hint":"The text content of the button.","name":"text","type":"string"},{"default":"","required":false,"hint":"If you want to use an image for the button pass in the link to it here (relative from the `images` folder).","name":"image","type":"string"},{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: `wheels=cool&x=y`). Please note that Wheels uses the `&` and `=` characters to split the parameters and encode them properly for you. However, if you need to pass in `&` or `=` as part of the value, then you need to encode them (and only them), example: `a=cats%26dogs%3Dtrouble!&b=1`.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"required":false,"hint":"The type of `method` to use in the `form` tag (`delete`, `get`, `patch`, `post`, and `put` are the options).","name":"method","type":"string"},{"default":true,"required":false,"hint":"If `true`, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"buttonTo","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Cache a single action (default 60 minutes)\ncaches(\"termsOfUse\");\n\n2. Cache multiple actions for 30 minutes\ncaches(actions=\"browseByUser, browseByTitle\", time=30);\n\n3. Cache actions as static pages, skipping filters\ncaches(actions=\"termsOfUse, codeOfConduct\", static=true);\n\n4. Cache content separately based on runtime variable\ncaches(action=\"home\", appendToKey=\"request.region\");
"},"hint":"Tells Wheels to cache one or more controller actions. Caching improves performance by storing the output of actions so that repeated requests do not require re-running the action logic.\n\n","returntype":"void","slug":"controller.caches","parameters":[{"default":"","required":false,"hint":"Action(s) to cache. This argument is also aliased as `actions`.","name":"action","type":"string"},{"default":60,"required":false,"hint":"Minutes to cache the action(s) for.","name":"time","type":"numeric"},{"default":false,"required":false,"hint":"Set to `true` to tell Wheels that this is a static page and that it can skip running the controller filters (before and after filters set on actions). Please note that the `onSessionStart` and `onRequestStart` events still execute though.","name":"static","type":"boolean"},{"default":"","required":false,"hint":"List of variables to be evaluated at runtime and included in the cache key so that content can be cached separately.","name":"appendToKey","type":"string"}],"availableIn":["controller"],"name":"caches","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Clear a single action from cache\nclearCachableActions(\"termsOfUse\");\n\n2. Clear multiple actions from cache\nclearCachableActions(actions=\"termsOfUse,codeOfConduct\");\n\n3. Clear all cacheable actions in the controller\nclearCachableActions();
"},"hint":"Removes one or more actions from the list of cacheable actions in a controller. Use this when you want to prevent previously cached actions from being cached or to reset caching for certain actions.","returntype":"void","slug":"controller.clearCachableActions","parameters":[{"default":"","required":false,"hint":"Action(s) to remove from cache.","name":"action","type":"string"}],"availableIn":["controller"],"name":"clearCachableActions","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Capitalize a single sentence\n#capitalize(\"wheels is a framework\")#\n\n\n2. Capitalize a name\n#capitalize(\"john doe\")#\n\n\n3. Capitalize a title\n#capitalize(\"introduction to wheels framework\")#\n
"},"hint":"Capitalizes the first letter of every word in the provided text, creating a nicely formatted title or sentence.\n\n","returntype":"string","slug":"controller.capitalize","parameters":[{"required":true,"name":"text","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"capitalize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Alter a table to add new columns\nt = changeTable(name='employees');\nt.string(columnNames=\"fullName\", default=\"\", allowNull=true, limit=\"255\");\nt.change();\n\n
"},"hint":"Used in migrations to alter an existing table in the database. This function allows you to modify the structure of a table, such as adding, modifying, or removing columns.\n\n","returntype":"void","slug":"tabledefinition.change","parameters":[{"default":"false","required":false,"name":"addColumns","type":"boolean"}],"availableIn":["tabledefinition"],"name":"change","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Change the type and limit of a column\nchangeColumn(table='members', columnName='status', columnType='string', limit=50);\n\n2. Change a decimal column’s precision and scale\nchangeColumn(table='products', columnName='price', columnType='decimal', precision=10, scale=2);\n\n3. Change a column to allow NULL and set a default value\nchangeColumn(table='users', columnName='nickname', columnType='string', limit=100, allowNull=true, default='Guest');\n\n4. Move a column to a specific position in the table\nchangeColumn(table='orders', columnName='status', columnType='string', limit=20, afterColumn='orderDate');
"},"hint":"Changes the definition of an existing column in a database table. This function is used in migration CFCs to update column properties such as type, size, default value, nullability, precision, and scale.\n\n","returntype":"void","slug":"migration.changeColumn","parameters":[{"required":true,"hint":"The Name of the table where the column is","name":"table","type":"string"},{"required":true,"hint":"THe name of the column","name":"columnName","type":"string"},{"required":true,"hint":"The type of the column","name":"columnType","type":"string"},{"default":"","required":false,"hint":"The name of the column which this column should be inserted after","name":"afterColumn","type":"string"},{"default":"","required":false,"hint":"Name for reference column, see documentation for references function, required if columnType is 'reference'","name":"referenceName","type":"string"},{"required":false,"hint":"Default value for this column","name":"default","type":"string"},{"required":false,"hint":"Whether to allow NULL values","name":"allowNull","type":"boolean"},{"required":false,"hint":"Character or integer size limit for column","name":"limit","type":"numeric"},{"required":false,"hint":"(For decimal type) the maximum number of digits allow","name":"precision","type":"numeric"},{"required":false,"hint":"(For decimal type) the number of digits to the right of the decimal point","name":"scale","type":"numeric"},{"default":"false","required":false,"hint":"if true, attempts to add columns and database will likely throw an error if column already exists","name":"addColumns","type":"boolean"}],"availableIn":["migration"],"name":"changeColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Track changes on a single property\nmember = model(\"member\").findByKey(params.memberId);\nmember.email = params.newEmail;\n\n// Get the previous value of the email\noldValue = member.changedFrom(\"email\");\n\n2. Using dynamic property function\n// Dynamic method naming also works\noldValue = member.emailChangedFrom();\n\n3. Check before saving\nmember.firstName = \"Bruce\";\n\nif (member.changedFrom(\"firstName\") != \"\") {\n    writeOutput(\"First name was changed from \" & member.changedFrom(\"firstName\"));\n}\n\nmember.save();
"},"hint":"Returns the previous value of a property that has been modified on a model object. Wheels tracks changes to object properties until the object is saved to the database. If no previous value exists (the property was never modified), it returns an empty string. This is useful for auditing, logging, or conditional logic based on changes to object properties.\n\n","returntype":"string","slug":"model.changedFrom","parameters":[{"required":true,"hint":"Name of property to get the previous value for.","name":"property","type":"string"}],"availableIn":["model"],"name":"changedFrom","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Track changed properties\nmember = model(\"member\").findByKey(params.memberId);\nmember.firstName = params.newFirstName;\nmember.email = params.newEmail;\n\n// Get a list of properties that have changed\nchangedProperties = member.changedProperties();\n\n2. Conditional logic based on changes\nif (arrayLen(member.changedProperties()) > 0) {\n    writeOutput(\"The following fields were changed: \" & arrayToList(member.changedProperties()));\n}
"},"hint":"Returns a list of property names that have been modified on a model object but not yet saved to the database. This is useful for tracking which fields were updated, triggering specific actions based on changes, or performing conditional validation.\n\n","returntype":"string","slug":"model.changedProperties","parameters":[],"availableIn":["model"],"name":"changedProperties","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Add new columns to an existing table\nt = changeTable(name='employees');\nt.string(columnNames=\"fullName\", default=\"\", allowNull=true, limit=255);\nt.boolean(columnNames=\"isActive\", default=true);\nt.change();\n\n2. Modify multiple columns\nt = changeTable(name='products');\nt.string(columnNames=\"productName\", limit=150, allowNull=false);\nt.decimal(columnNames=\"price\", precision=10, scale=2);\nt.change();
"},"hint":"Creates a table definition object used to store and apply modifications to an existing table in the database. This function is only available inside a migration CFC and works in conjunction with table definition methods like string(), integer(), boolean(), etc., and the change() method to apply the changes.\n\n","returntype":"TableDefinition","slug":"migration.changeTable","parameters":[{"required":true,"hint":"Name of the table to set change properties on","name":"name","type":"string"}],"availableIn":["migration"],"name":"changeTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single CHAR column\nt.char(columnNames=\"status\", limit=1, default=\"A\", allowNull=false);\n\n2. Add multiple CHAR columns\nt.char(columnNames=\"type,code\", limit=2, default=\"\", allowNull=true);\n\n3. Add a CHAR column without a limit\nt.char(columnNames=\"initials\", allowNull=true);
"},"hint":"Adds one or more CHAR columns to a table definition in a migration. Use this function to define fixed-length string columns when creating or modifying a table.\n\n","returntype":"any","slug":"tabledefinition.char","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"any"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"char","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic checkbox for a single boolean property\n#checkBox(objectName="photo", property="isPublic", label="Display this photo publicly.")#\n\n2. Checkbox for a nested hasMany association\n<cfloop from="1" to="#ArrayLen(user.photos)#" index="i">\n    <div>\n        <h3>#user.photos[i].title#:</h3>\n        <div>\n            #checkBox(objectName="user", association="photos", position=i, property="isPublic", label="Display this photo publicly.")#\n        </div>\n    </div>\n</cfloop>
"},"hint":"Builds and returns a string containing a check box form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.checkBox","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":1,"required":false,"name":"checkedValue","type":"string"},{"default":0,"required":false,"hint":"The value of the check box when it's on the unchecked state.","name":"uncheckedValue","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"checkBox","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic checkbox\n#checkBoxTag(name="subscribe", value="true", label="Subscribe to our newsletter", checked=false)#\n\n2. Checkboxes generated from a query\n// Controller code\npizza = model("pizza").findByKey(session.pizzaId);\nselectedToppings = pizza.toppings();\ntoppings = model("topping").findAll(order="name");\n\n<!--- View code --->\n<fieldset>\n\t<legend>Toppings</legend>\n\t<cfoutput query="toppings">\n\t\t#checkBoxTag(name="toppings", value="true", label=toppings.name, checked=YesNoFormat(ListFind(ValueList(selectedToppings.id), toppings.id))#\n\t</cfoutput>\n</fieldset>
"},"hint":"Builds and returns a string containing a check box form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.checkBoxTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":false,"required":false,"hint":"Whether or not the check box should be checked by default.","name":"checked","type":"boolean"},{"default":1,"required":false,"hint":"Value of check box in its checked state.","name":"value","type":"string"},{"default":"","required":false,"hint":"The value of the check box when it's on the unchecked state.","name":"uncheckedValue","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"checkBoxTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Clear change information for a single property\n// Convert startTime to UTC in an \"afterFind\" callback\nthis.startTime = DateConvert(\"Local2UTC\", this.startTime);\n\n// Tell Wheels to clear internal change tracking for this property\nthis.clearChangeInformation(property=\"startTime\");\n\n2. Clear change information for all properties\n// Clear internal tracking for all properties of the object\nthis.clearChangeInformation();
"},"hint":"Clears all internal tracking information that Wheels maintains about an object’s properties. This does not undo changes made to the object—it simply resets the record of which properties are considered “changed,” so methods like hasChanged(), changedProperties(), or allChanges() will no longer report them. This is useful when you modify a property programmatically (for example, in a callback) and don’t want Wheels to attempt saving or reporting it as a change.\n\n","returntype":"void","slug":"model.clearChangeInformation","parameters":[{"required":false,"hint":"string false Name of property to clear information for.","name":"property","type":"string"}],"availableIn":["model"],"name":"clearChangeInformation","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Clear all errors on the object\n// Remove all errors regardless of property\nthis.clearErrors();\n\n2. Clear errors on a specific property\n// Remove all errors associated with the 'firstName' property\nthis.clearErrors(property=\"firstName\");\n\n3. Clear a specific error by name\n// Remove only the error named 'emailFormatError' without affecting other errors\nthis.clearErrors(name=\"emailFormatError\");
"},"hint":"Clears all validation or manual errors stored on a model object. You can clear all errors, or target specific errors either by property name or by a custom error name. This is useful when resetting an object’s state before re-validation, updating values programmatically, or handling conditional validation logic.\n\n","returntype":"void","slug":"model.clearErrors","parameters":[{"default":"","required":false,"hint":"Specify a property name here if you want to clear all errors set on that property.","name":"property","type":"string"},{"default":"","required":false,"hint":"Specify an error name here if you want to clear all errors set with that error name.","name":"name","type":"string"}],"availableIn":["model"],"name":"clearErrors","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
\n<cfscript>\n\nmapper()\n    // Create a route like `photos/search`\n    .resources(name="photos", nested=true)\n        .collection()\n            .get("search")\n        .end()\n    .end()\n.end();\n\n</cfscript>\n
"},"hint":"Defines a collection route in your Wheels application. Collection routes operate on a set of resources and do not require an id, unlike member routes which act on a single resource. This is useful when building actions that retrieve, filter, or display multiple objects, such as search pages, listings, or batch operations.\n\n","returntype":"struct","slug":"mapper.collection","parameters":[],"availableIn":["mapper"],"name":"collection","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Add a string column\nt = changeTable(name=\"employees\");\nt.column(columnName=\"fullName\", columnType=\"string\", limit=255, allowNull=false, default=\"Unknown\");\nt.change();\n\n2. Add a decimal column\nt = changeTable(name=\"products\");\nt.column(columnName=\"price\", columnType=\"decimal\", precision=10, scale=2, allowNull=false, default=\"0.00\");\nt.change();\n\n3. Add a boolean column\nt = changeTable(name=\"members\");\nt.column(columnName=\"isActive\", columnType=\"boolean\", allowNull=false, default=\"1\");\nt.change();
"},"hint":"Adds a column to a table definition in a migration. This function is used when defining or altering database tables. It supports multiple column types and allows you to specify constraints like default values, nullability, length, and precision. Use this inside a table definition object in a migration CFC when building or modifying tables.\n\n","returntype":"any","slug":"tabledefinition.column","parameters":[{"required":true,"name":"columnName","type":"string"},{"required":true,"name":"columnType","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"},{"required":false,"name":"limit","type":"any"},{"required":false,"name":"precision","type":"numeric"},{"required":false,"name":"scale","type":"numeric"}],"availableIn":["tabledefinition"],"name":"column","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Inspect a simple property\nuser = model(\"user\").columnDataForProperty(\"email\");\n\nwriteDump(user);\n\nOutput might include:\n{\n  \"column\": \"email\",\n  \"dataType\": \"string\",\n  \"columnDefault\": \"\",\n  \"nullable\": \"NO\",\n  \"size\": 255\n}\n\n2. Use column metadata for validation or dynamic forms\ncolumns = model(\"product\").columnDataForProperty(\"price\");\n\nif(columns.nullable EQ \"NO\" AND columns.dataType EQ \"decimal\") {\n    writeOutput(\"Price is required and must be decimal.\");\n}
"},"hint":"Returns a struct containing metadata about a specific property in a model. This includes information such as type, constraints, default values, and other column-specific details. It’s useful when you need to introspect the schema of your model dynamically.\n\n","returntype":"any","slug":"model.columnDataForProperty","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"columnDataForProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Retrieve the column name for a property\nuser = model(\"user\").columnForProperty(\"email\");\n\nwriteOutput(user);  // Might output: \"email_address\"\n\n2. Use in dynamic SQL queries\nuserModel = model(\"user\");\ncolumn = userModel.columnForProperty(\"firstName\");\nquery = \"SELECT #column# FROM users WHERE id = 1\";
"},"hint":"Returns the database column name that corresponds to a given model property. This is useful when your model property names differ from the actual database column names, or when you need to dynamically generate SQL queries or mappings.\n\n","returntype":"any","slug":"model.columnForProperty","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"columnForProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get all column names for a model\nuserModel = model(\"user\");\ncolumns = userModel.columnNames();\n\nwriteOutput(columns);\n// Might output: \"id,first_name,last_name,email,created_at,updated_at\"\n\n2. Use column names to dynamically select fields in a query\nuserModel = model(\"user\");\nqueryColumns = userModel.columnNames();\nq = \"SELECT #queryColumns# FROM users WHERE active = 1\";
"},"hint":"Returns a list of column names for the table mapped to this model. The list is ordered according to the columns’ ordinal positions in the database table. This is useful for dynamically generating queries, forms, or for inspecting the database structure associated with a model.\n\n","returntype":"string","slug":"model.columnNames","parameters":[],"availableIn":["model"],"name":"columnNames","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get an array of columns for a model\nuserModel = model(\"user\");\ncolumnArray = userModel.columns();\n\nwriteDump(columnArray);\n// Might output: [\"id\", \"first_name\", \"last_name\", \"email\", \"created_at\", \"updated_at\"]\n\n2. Loop through the columns for dynamic processing\nuserModel = model(\"user\");\nfor(column in userModel.columns()) {\n    writeOutput(\"Column: #column#
\");\n}
"},"hint":"Returns an array of database column names for the table associated with the model. This method excludes calculated or transient properties that are defined in the model but not stored in the database.\n\n","returntype":"array","slug":"model.columns","parameters":[],"availableIn":["model"],"name":"columns","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Compare two user objects\nuser1 = model(\"user\").findByKey(1);\nuser2 = model(\"user\").findByKey(2);\n\nif(user1.compareTo(user2)) {\n    writeOutput(\"Objects are the same.\");\n} else {\n    writeOutput(\"Objects are different.\");\n}\n\n2. Compare dynamically after changing a property\nuser1 = model(\"user\").findByKey(1);\nuser2 = model(\"user\").findByKey(1);\n\nuser2.email = \"[newemail@example.com](mailto:newemail@example.com)\";\n\nwriteDump(user1.compareTo(user2)); // Will output false because email changed
"},"hint":"Compares the current model object with another model object to determine if they are effectively the same. This is useful for checking equality between two instances of the same model before performing operations like updates or merges.\n\n","returntype":"boolean","slug":"model.compareTo","parameters":[{"required":true,"name":"object","type":"component"}],"availableIn":["model"],"name":"compareTo","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Constrain a route parameter to digits only\nmapper()\n    .resources(name=\"users\", nested=true)\n        .member(id=\":userId\")\n            .constraints({ userId=\"^\\d+$\" })\n        .end()\n    .end()\n.end();\n\nHere, the userId parameter must be a number, otherwise the route won’t match.\n\n2. Constrain multiple parameters\nmapper()\n    .resources(name=\"orders\", nested=true)\n        .member(orderId=\":orderId\", itemId=\":itemId\")\n            .constraints({ \n                orderId=\"^\\d+$\", \n                itemId=\"^\\d{3}-[A-Z]{2}$\" \n            })\n        .end()\n    .end()\n.end();
"},"hint":"Defines variable patterns for route parameters when setting up routes using the Wheels mapper(). This allows you to restrict the values that route parameters can take, such as limiting an id parameter to numbers only or enforcing a specific string format.\n\n","returntype":"struct","slug":"mapper.constraints","parameters":[],"availableIn":["mapper"],"name":"constraints","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\n<!--- In your view --->\n<cfsavecontent variable=\"mySidebar\">\n    <h1>My Sidebar Text</h1>\n</cfsavecontent>\n\n<cfset contentFor(sidebar=mySidebar)>\n\n<!--- In your layout --->\n<html>\n    <head><title>My Site</title></head>\n    <body>\n        <cfoutput>\n            #includeContent(\"sidebar\")#  <!-- Renders the sidebar content -->\n            #includeContent()#           <!-- Renders main content -->\n        </cfoutput>\n    </body>\n</html>\n\n2. Adding multiple pieces to the same section\n<cfset contentFor(sidebar=\"First piece of content\")>\n<cfset contentFor(sidebar=\"Second piece of content\", position=\"first\")>\n\n<!--- Renders 'Second piece of content' first, then 'First piece of content' -->\n#includeContent(\"sidebar\")#\n\n3. Overwriting content\n<cfset contentFor(sidebar=\"Old content\")>\n<cfset contentFor(sidebar=\"New content\", overwrite=true)>\n\n<!--- Only 'New content' will be rendered -->\n#includeContent(\"sidebar\")#
"},"hint":"contentFor() is used to store a section's output in a layout. It allows you to define content in your view templates and then render it in a layout using #includeContent()#. The function maintains a stack for each section, so multiple pieces of content can be added in a controlled order.\n\n","returntype":"void","slug":"controller.contentFor","parameters":[{"default":"last","required":false,"hint":"The position in the section's stack where you want the content placed. Valid values are `first`, `last`, or the numeric position.","name":"position","type":"any"},{"default":"false","required":false,"hint":"Whether or not to overwrite any of the content. Valid values are `false`, `true`, or `all`.","name":"overwrite","type":"any"}],"availableIn":["controller"],"name":"contentFor","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Controller:\n// PostsController.cfc\nfunction show() {\n    var post = model(\"post\").findByKey(params.id);\n}\n\nView (views/posts/show.cfm):\n<h2>#post.title#</h2>\n<p>#post.body#</p>\n\nLayout (views/layout.cfm):\n<html>\n<head>\n    <title>Blog</title>\n</head>\n<body>\n    <nav>Home | Posts</nav>\n\n    <!-- Inject view content -->\n    #contentForLayout()#\n\n    <footer>© 2025 My Blog</footer>\n</body>\n</html>\n\nOutput when visiting /posts/show?id=1:\n<html>\n<head>\n    <title>Blog</title>\n</head>\n<body>\n    <nav>Home | Posts</nav>\n\n    <h2>Hello World</h2>\n    <p>This is my first post!</p>\n\n    <footer>© 2025 My Blog</footer>\n</body>\n</html>
"},"hint":"contentForLayout() is used to render the main content of the current view inside a layout. In Wheels, when a controller action renders a view, that view generates content. This content can then be injected into the layout at the appropriate place using contentForLayout(). Essentially, it’s the placeholder for the view’s body content in your layout template.\n\n","returntype":"string","slug":"controller.contentForLayout","parameters":[],"availableIn":["controller"],"name":"contentForLayout","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":false,"docs":""},"hint":"The controller() function in Wheels is used to define routes that point to a specific controller. However, it is considered deprecated, because it does not align with RESTful routing principles. Wheels encourages using resources() and other RESTful routing helpers instead.\n\n","returntype":"struct","slug":"mapper.controller","parameters":[{"required":true,"name":"controller","type":"string"},{"default":"[runtime expression]","required":false,"name":"name","type":"string"},{"default":"[runtime expression]","required":false,"name":"path","type":"string"}],"availableIn":["mapper"],"name":"controller","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
testController = controller("users", params);
"},"hint":"The controller() function creates and returns a controller object with a custom name and optional parameters. It is primarily used for testing, but can also be used in code to instantiate a controller programmatically. Unlike the deprecated routing controller() function, this helper does not define routes—it creates controller instances.\n\n","returntype":"any","slug":"controller.controller","parameters":[{"required":true,"hint":"Name of the controller to create.","name":"name","type":"string"},{"default":"[runtime expression]","required":false,"hint":"The params struct (combination of form and URL variables).","name":"params","type":"struct"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"controller","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Count how many authors there are in the table\nauthorCount = model("author").count();\n\n2. Count how many authors that have a last name starting with an "A"\nauthorOnACount = model("author").count(where="lastName LIKE 'A%'");\n\n3. Count how many authors that have written books starting with an "A"\nauthorWithBooksOnACount = model("author").count(include="books", where="booktitle LIKE 'A%'");\n\n4. Count the number of comments on a specific post (a `hasMany` association from `post` to `comment` is required)\n// The `commentCount` method will call `model("comment").count(where="postId=#post.id#")` internally\naPost = model("post").findByKey(params.postId);\namount = aPost.commentCount();
"},"hint":"The count() method calculates the number of records in a table that match a given set of conditions. It internally uses the SQL COUNT() function. If no arguments are provided, it returns the total number of rows in the table. It works on model classes.\n\n","returntype":"any","slug":"model.count","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"count","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
t = table(name=\"employees\");\nt.string(columnNames=\"firstName\", limit=50, allowNull=false);\nt.string(columnNames=\"lastName\", limit=50, allowNull=false);\nt.integer(columnNames=\"age\", allowNull=true);\nt.boolean(columnNames=\"isActive\", default=\"1\");\n\n// Create the table in the database\nt.create();
"},"hint":"The create() method is used to create a database table based on the table definition that has been built using the migrator’s table definition functions (string(), integer(), boolean(), etc.). This method is only available within a migration CFC and finalizes the table creation in the database.\n\n","returntype":"void","slug":"tabledefinition.create","parameters":[],"availableIn":["tabledefinition"],"name":"create","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Create a new author and save it to the database\nnewAuthor = model("author").create(params.author);\n\n2. Same as above using named arguments\nnewAuthor = model("author").create(firstName="John", lastName="Doe");\n\n3. Same as above using both named arguments and a struct\nnewAuthor = model("author").create(active=1, properties=params.author);\n\n4. If you have a `hasOne` or `hasMany` association setup from `customer` to `order`, you can do a scoped call. (The `createOrder` method below will call `model("order").create(customerId=aCustomer.id, shipping=params.shipping)` internally.)\naCustomer = model("customer").findByKey(params.customerId);\nanOrder = aCustomer.createOrder(shipping=params.shipping);
"},"hint":"The create() method is used to instantiate a new model object, set its properties, and save it to the database (if validations pass). Even if validation fails, the method still returns the unsaved object, including any validation errors. It’s a higher-level convenience function that combines object creation, property assignment, validation, and saving into a single call. Property names and values can be passed in either using named arguments or as a struct to the properties argument.\n\n","returntype":"any","slug":"model.create","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to allow explicit assignment of `createdAt` or `updatedAt` properties","name":"allowExplicitTimestamps","type":"boolean"}],"availableIn":["model"],"name":"create","tags":{"categoryClass":"createfunctions","sectionClass":"modelclass","category":"Create Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Create an empty migration file:\n\nresult = application.wheels.migrator.createMigration(\"MyMigrationFile\");\n\n// Generates a blank migration file with a timestamped prefix.\n\n// You can then edit it to define your table or schema changes.\n\n2. Create a migration file from a template (e.g., create-table):\n\nresult = application.wheels.migrator.createMigration(\"MyMigrationFile\", \"create-table\");\n\n// Generates a migration file pre-populated with a create-table template.
"},"hint":"The createMigration() method is used to generate a new migration file for managing database schema changes. While you can call it from your application code, it is primarily intended for use via the CLI or Wheels GUI. A migration file allows you to define table creations, modifications, or deletions in a structured way that can be applied or rolled back consistently.\n\n","returntype":"string","slug":"migrator.createMigration","parameters":[{"required":true,"name":"migrationName","type":"string"},{"default":"","required":false,"name":"templateName","type":"string"},{"default":"timestamp","required":false,"name":"migrationPrefix","type":"string"}],"availableIn":["migrator"],"name":"createMigration","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
// Example: create a users table\nt = createTable(name='users'); \n\tt.string(columnNames='firstname,lastname', default='', allowNull=false, limit=50);\n\tt.string(columnNames='email', default='', allowNull=false, limit=255); \n\tt.string(columnNames='passwordHash', default='', allowNull=true, limit=500);\n\tt.string(columnNames='passwordResetToken,verificationToken', default='', allowNull=true, limit=500);\n\tt.boolean(columnNames='passwordChangeRequired,verified', default=false); \n\tt.datetime(columnNames='passwordResetTokenAt,passwordResetAt,loggedinAt', default='', allowNull=true); \n\tt.integer(columnNames='roleid', default=0, allowNull=false, limit=3);\n\tt.timestamps();\nt.create();\n\n// Example: Create a table with a different Primary Key\nt = createTable(name='tokens', id=false);\n\tt.primaryKey(name='id', allowNull=false, type="string", limit=35 );\n\tt.datetime(columnNames="expiresAt", allowNull=false);\n\tt.integer(columnNames='requests', default=0, allowNull=false);\n\tt.timestamps();\nt.create();\n\n// Example: Create a Join Table with composite primary keys\nt = createTable(name='userkintins', id=false); \n\tt.primaryKey(name="userid", allowNull=false, limit=11);\n\tt.primaryKey(name='profileid', type="string", limit=11 );  \nt.create();\n
"},"hint":"The createTable() function is used in migration CFCs to define a new database table. It returns a TableDefinition object, on which you can specify columns, primary keys, timestamps, and other table properties. Once the table is defined, you call create() to actually create it in the database.\n\n","returntype":"TableDefinition","slug":"migration.createTable","parameters":[{"required":true,"hint":"The name of the table to create","name":"name","type":"string"},{"default":"false","required":false,"hint":"whether to drop the table before creating it","name":"force","type":"boolean"},{"default":"true","required":false,"hint":"Whether to create a default primarykey or not","name":"id","type":"boolean"},{"default":"id","required":false,"hint":"Name of the primary key field to create","name":"primaryKey","type":"string"}],"availableIn":["migration"],"name":"createTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Simple View Creation\n\nv = createView(name='active_users');\nv.selectStatement(sql = \"SELECT * FROM c_o_r_e_users\")\nv.create();\n\n// Creates a view active_users that selects only active users from the users table.\n\n2. View with Join\n\nv = createView(name='user_orders');\nv.selectStatement(sql = \"SELECT u.id, u.firstname, u.lastname, o.id AS orderId, o.total FROM users u JOIN orders o ON u.id = o.userId WHERE o.status = \"completed\";\")\nv.create();\n\n// Creates a user_orders view joining users and orders tables, filtering only completed orders.
"},"hint":"The createView() function is used in migration CFCs to define a new database view. It returns a ViewDefinition object, on which you can specify the view’s SQL query and properties. Once the view is fully defined, you call create() to actually create it in the database.\n\n","returntype":"ViewDefinition","slug":"migration.createView","parameters":[{"required":true,"hint":"Name of the view to change properties on","name":"name","type":"string"}],"availableIn":["migration"],"name":"createView","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<head>\n    <title>My Application</title>\n    #csrfMetaTags()#\n</head>\n\n// This will output something like:\n// <meta name=\"csrf-token\" content=\"YOUR_AUTH_TOKEN_HERE\">\n// <meta name=\"csrf-param\" content=\"authenticityToken\">
"},"hint":"The csrfMetaTags() helper generates meta tags containing your application's CSRF authenticity token. This is useful for JavaScript/AJAX requests that need to POST data securely, ensuring that the request comes from a trusted source.\n\n","returntype":"string","slug":"controller.csrfMetaTags","parameters":[],"availableIn":["controller"],"name":"csrfMetaTags","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<!--- Alternating table row colors --->\n<table>\n\t<thead>\n\t\t<tr>\n\t\t\t<th>Name</th>\n\t\t\t<th>Phone</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t<cfoutput query="employees">\n\t\t\t<tr class="#cycle("odd,even")#">\n\t\t\t\t<td>#employees.name#</td>\n\t\t\t\t<td>#employees.phone#</td>\n\t\t\t</tr>\n\t\t</cfoutput>\n\t</tbody>\n</table>\n\n<!--- Alternating row colors and shrinking emphasis --->\n<cfoutput query="employees" group="departmentId">\n\t<div class="#cycle(values="even,odd", name="row")#">\n\t\t<ul>\n\t\t\t<cfoutput>\n\t\t\t\trank = cycle(values="president,vice-president,director,manager,specialist,intern", name="position")>\n\t\t\t\t<li class="#rank#">#categories.categoryName#</li>\n\t\t\t\tresetCycle("emphasis")>\n\t\t\t</cfoutput>\n\t\t</ul>\n\t</div>\n</cfoutput>
"},"hint":"cycle() is a view helper used to loop through a list of values sequentially, returning the next value each time it’s called. This is especially useful for things like alternating row colors in tables or assigning sequential classes in repeated HTML elements.\n\n","returntype":"string","slug":"controller.cycle","parameters":[{"required":true,"hint":"List of values to cycle through.","name":"values","type":"string"},{"default":"default","required":false,"hint":"Name to give the cycle. Useful when you use multiple cycles on a page.","name":"name","type":"string"}],"availableIn":["controller"],"name":"cycle","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
// In app/models/User.cfc\ncomponent extends=\"Model\" {\n\n    function config() {\n        // Use a custom datasource for this model\n        dataSource(\"users_source\");\n        \n        // Optional: specify credentials\n        // dataSource(\"users_source\", \"dbUser\", \"dbPass\");\n    }\n}
"},"hint":"dataSource() is a model configuration method used to override the default database connection for a specific model. This is useful when you want a model to query a different database or use specific credentials than the application default.\n\n","returntype":"void","slug":"model.dataSource","parameters":[{"required":true,"hint":"The data source name to connect to.","name":"datasource","type":"string"},{"default":"","required":false,"hint":"The username for the data source.","name":"username","type":"string"},{"default":"","required":false,"hint":"The password for the data source.","name":"password","type":"string"}],"availableIn":["model"],"name":"dataSource","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// In a migration CFC\nt = createTable(name=\"events\");\nt.date(columnNames=\"startDate,endDate\",  default=\"\",  allowNull=false);\nt.create();
"},"hint":"date() is a table definition function used in a migration CFC to add one or more DATE columns to a table.\n\n","returntype":"any","slug":"tabledefinition.date","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"date","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateSelect(objectName=\"user\", property=\"dateOfBirth\")#\n\nOutputs month/day/year selects for the user.dateOfBirth property.\n\n---\n\nExample 2: Only month and year (no day)\n#dateSelect(objectName=\"order\", property=\"expirationDate\", order=\"month,year\")#\n\nUseful for credit card expiration dates.\n\nOnly month and year dropdowns appear.\n\n---\n\nExample 3: Custom year range\n#dateSelect(objectName=\"event\", property=\"eventDate\", startYear=2000, endYear=2030)#\n\nDropdown shows years 2000–2030.\n\n---\n\nExample 4: Custom month display\n#dateSelect(objectName=\"user\", property=\"anniversary\", monthDisplay=\"abbreviations\")#\n\nMonths display as Jan, Feb, Mar… instead of full names.\n\n---\n\nExample 5: Include blank options\n#dateSelect(objectName=\"profile\", property=\"graduationDate\", includeBlank=\"- Select Date -\")#\n\nAdds a blank option at the top of each select with the label - Select Date -.\n\n---\n\nExample 6: Using labels and custom HTML\n#dateSelect(objectName=\"employee\", property=\"hireDate\", label=\"Hire Date\", labelPlacement=\"before\", prepend=\"<div class='date-wrapper'>\", append=\"</div>\")#\n\nAdds a label and wraps selects inside a div for styling.
"},"hint":"Builds and returns a string containing three select form controls for month, day, and year based on the supplied objectName and property.\n\n","returntype":"string","slug":"controller.dateSelect","parameters":[{"default":"","required":false,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"default":"","required":false,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a `hasMany` relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date `select` tags.","name":"order","type":"string"},{"default":" ","required":false,"name":"separator","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"name":"monthAbbreviations","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the `select` form control. Pass `true` to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using `aroundLeft` or `aroundRight`.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The `class` name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"dateSelect","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateSelectTags(name=\"dateStart\", selected=params.dateStart)#\n\nOutputs month/day/year selects with the value pre-selected from params.dateStart.\n\n---\n\nExample 2: Month and year only\n#dateSelectTags(name=\"expiration\", selected=params.expiration, order=\"month,year\")#\n\nUseful for credit card expiration date inputs.\n\nOnly month and year dropdowns appear.\n\n---\n\nExample 3: Custom year range\n#dateSelectTags(name=\"eventDate\", startYear=2000, endYear=2030)#\n\nDropdown shows years 2000–2030.\n\n---\n\nExample 4: Custom month display\n#dateSelectTags(name=\"anniversary\", monthDisplay=\"abbreviations\")#\n\nMonths display as Jan, Feb, Mar… instead of full names.\n\n---\n\nExample 5: Include blank options\n#dateSelectTags(name=\"graduationDate\", includeBlank=\"- Select Date -\")#\n\nAdds a blank option at the top of each dropdown with - Select Date -.\n\n---\n\nExample 6: Using labels and custom HTML\n#dateSelectTags(name=\"hireDate\", label=\"Hire Date\", labelPlacement=\"before\", prepend=\"<div class='date-wrapper'>\", append=\"</div>\")#\n\nAdds a label and wraps selects inside a <div> for styling.
"},"hint":"dateSelectTags() is similar to dateSelect(), but instead of binding to a model object, it works directly with a name and selected value. It generates three select dropdowns (month, day, year) for form tags.\n\n","returntype":"string","slug":"controller.dateSelectTags","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date `select` tags.","name":"order","type":"string"},{"default":" ","required":false,"hint":"[see:dateSelect].","name":"separator","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"hint":"[see:dateSelect].","name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"hint":"[see:dateSelect].","name":"monthAbbreviations","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"dateSelectTags","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\nt = createTable(name=\"appointments\"); \nt.datetime(columnNames=\"startAt,endAt\");\nt.create();\n\nCreates startAt and endAt columns as datetime columns in the appointments table.\n\n---\n\nExample 2: With NULL allowed\nt = createTable(name=\"events\"); \nt.datetime(columnNames=\"cancelledAt\", allowNull=true);\nt.create();\n\ncancelledAt column allows NULL values.\n\n---\n\nExample 3: With default timestamp\nt = createTable(name=\"logs\"); \nt.datetime(columnNames=\"createdAt\", default=\"CURRENT_TIMESTAMP\");\nt.create();\n\nSets createdAt to the current timestamp by default.\n\n---\n\nExample 4: Multiple datetime columns with defaults\nt = createTable(name=\"tasks\"); \nt.datetime(columnNames=\"assignedAt,completedAt\", default=\"CURRENT_TIMESTAMP\", allowNull=false);\nt.create();\n\nBoth columns are non-nullable and default to the current timestamp.
"},"hint":"Adds datetime columns to a table definition when creating or altering a table in a migration. These columns store both date and time values.\n\n","returntype":"any","slug":"tabledefinition.datetime","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"datetime","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateTimeSelect(objectName=\"article\", property=\"publishedAt\")#\n\nGenerates all six selects for article.publishedAt.\n\n---\n\nExample 2: Custom date and time order\n#dateTimeSelect(objectName=\"appointment\", property=\"dateTimeStart\", dateOrder=\"month,day\", timeOrder=\"hour,minute\")#\n\nOnly shows month & day for date and hour & minute for time.\n\n---\n\nExample 3: 12-hour format with AM/PM\n#dateTimeSelect(objectName=\"meeting\", property=\"startTime\", twelveHour=true, timeOrder=\"hour,minute\")#\n\nHours dropdown uses 1–12 with AM/PM options.\n\n---\n\nExample 4: Include blank options and custom year range\n#dateTimeSelect(objectName=\"event\", property=\"eventTime\", startYear=2020, endYear=2030, includeBlank=true)#\n\nAdds an empty option for each select and sets the year range to 2020–2030.\n\n---\n\nExample 5: Custom separators\n#dateTimeSelect(objectName=\"flight\", property=\"departure\", dateSeparator=\"/\", timeSeparator=\".\")#\n\nShows / between date selects and . between time selects.
"},"hint":"Builds and returns a string containing six select form controls (three for date selection and the remaining three for time selection) based on the supplied objectName and property.\n\n","returntype":"string","slug":"controller.dateTimeSelect","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"string"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date select tags.","name":"dateOrder","type":"string"},{"default":" ","required":false,"name":"dateSeparator","type":"string"},{"default":2018,"required":false,"hint":"Last year in select list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"name":"monthAbbreviations","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"timeOrder","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"timeSeparator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc","name":"secondStep","type":"numeric"},{"default":" - ","required":false,"hint":"Use to change the character that is displayed between the first and second set of select tags.","name":"separator","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"Whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"dateTimeSelect","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#dateTimeSelectTags(\n    name=\"dateTimeStart\",\n    selected=params.dateTimeStart\n)#\n\nGenerates six selects for date/time with default order and all fields included.\n\n---\n\nExample 2: Show only month, day, hour, and minute\n#dateTimeSelectTags(\n    name=\"dateTimeStart\",\n    selected=params.dateTimeStart,\n    dateOrder=\"month,day\",\n    timeOrder=\"hour,minute\"\n)#\n\nExcludes year and seconds from the dropdowns.\n\n---\n\nExample 3: 12-hour format with AM/PM\n#dateTimeSelectTags(\n    name=\"meetingTime\",\n    selected=params.meetingTime,\n    twelveHour=true,\n    timeOrder=\"hour,minute\"\n)#\n\nHours are displayed as 1–12 with AM/PM dropdown.\n\n---\n\nExample 4: Custom year range with blank options\n#dateTimeSelectTags(\n    name=\"eventTime\",\n    selected=params.eventTime,\n    startYear=2020,\n    endYear=2030,\n    includeBlank=true\n)#\n\nAdds blank options and limits year selection to 2020–2030.\n\n---\n\nExample 5: Custom separators between date and time\n#dateTimeSelectTags(\n    name=\"flightDeparture\",\n    selected=params.departure,\n    dateSeparator=\"/\",\n    timeSeparator=\".\"\n)#\n\nUses / between date selects and . between time selects.
"},"hint":"Builds and returns a string containing six select form controls (three for date selection and the remaining three for time selection) based on a name.\n\n","returntype":"string","slug":"controller.dateTimeSelectTags","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":"month,day,year","required":false,"hint":"Use to change the order of or exclude date select tags.","name":"dateOrder","type":"string"},{"default":" ","required":false,"hint":"[see:dateTimeSelect].","name":"dateSeparator","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"hint":"[see:dateSelect].","name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"hint":"[see:dateSelect].","name":"monthAbbreviations","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"timeOrder","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"timeSeparator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":" - ","required":false,"hint":"Use to change the character that is displayed between the first and second set of select tags.","name":"separator","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"dateTimeSelectTags","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n#daySelectTag(name=\"dayOfWeek\", selected=params.dayOfWeek)#\n\nGenerates a standard select dropdown for all days of the week.\n\nPre-selects the value from params.dayOfWeek if available.\n\n---\n\nExample 2: Include a blank option\n#daySelectTag(name=\"meetingDay\", selected=params.meetingDay, includeBlank=true)#\n\nAdds a blank option at the top so users can select nothing.\n\n---\n\nExample 3: Custom label before the field\n#daySelectTag(\n    name=\"deliveryDay\",\n    selected=params.deliveryDay,\n    label=\"Choose delivery day:\",\n    labelPlacement=\"before\"\n)#\n\nAdds a label that appears before the dropdown.\n\n---\n\nExample 4: Prepend and append HTML\n#daySelectTag(\n    name=\"eventDay\",\n    prepend=\"<div class='select-wrapper'>\",\n    append=\"</div>\"\n)#\n\nWraps the dropdown inside a <div> for styling purposes.
"},"hint":"Builds and returns a string containing a select form control for the days of the week based on the supplied name. This version works without binding to a model object.\n\n","returntype":"string","slug":"controller.daySelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"daySelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n// In a test\nuser = model(\"user\").findByKey(1);\n\n// Inspect the user object\ndebug(user);\n\nDumps the contents of the user object to the test output.\n\n---\n\nExample 2: Debug without output\n// Evaluate an expression but don't output\nresult = someFunction();\ndebug(result, display=false);\n\nUseful when you want to leave the debug call in place for later but don’t want it to show in test output immediately.\n\n---\n\nExample 3: Debug an expression directly\ndebug(\"2 + 2\");\n\nQuickly examines a simple expression, like a calculation or string.
"},"hint":"Used in tests to inspect any expression. It behaves like a cfdump but is tailored for the testing environment. This helps you examine values while writing or running legacy tests.\n\n","returntype":"any","slug":"test.debug","parameters":[{"required":true,"hint":"The expression to examine","name":"expression","type":"string"},{"default":true,"required":false,"hint":"Whether to display the debug call. False returns without outputting anything into the buffer. Good when you want to leave the debug command in the test for later purposes, but don't want it to display","name":"display","type":"boolean"}],"availableIn":["test"],"name":"debug","tags":{"categoryClass":"testingfunctions","sectionClass":"testmodel","category":"Testing Functions","section":"Test Model"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic decimal column\nt = changeTable(\"products\");\nt.decimal(columnNames=\"price\", default=\"0.00\", allowNull=false, precision=10, scale=2);\nt.change();\n\nAdds a price column with up to 10 digits, 2 of which are after the decimal point, default 0.00, and cannot be NULL.\n\n---\n\nExample 2: Multiple decimal columns\nt = changeTable(\"invoices\");\nt.decimal(columnNames=\"tax,discount\", default=\"0.00\", allowNull=false, precision=8, scale=2);\nt.change();\n\nAdds tax and discount columns with the same configuration.\n\n---\n\nExample 3: Nullable decimal column with no default\nt = createTable(\"payments\");\nt.decimal(columnNames=\"amountDue\", allowNull=true, precision=12, scale=4);\nt.create();\n\nAdds a amountDue column that can be NULL and allows up to 12 digits, 4 of which are after the decimal.
"},"hint":"Adds decimal (numeric) columns to a table definition when creating or altering tables via a migration CFC.\n\n","returntype":"any","slug":"tabledefinition.decimal","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"},{"required":false,"name":"precision","type":"numeric"},{"required":false,"name":"scale","type":"numeric"}],"availableIn":["tabledefinition"],"name":"decimal","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  articleReview\n    // Example URL: /articles/987/reviews/12542\n    // Controller:  Reviews\n    // Action:      delete\n    .delete(name="articleReview", pattern="articles/[articleKey]/reviews/[key]", to="reviews##delete")\n\n    // Route name:  cookedBooks\n    // Example URL: /cooked-books\n    // Controller:  CookedBooks\n    // Action:      delete\n    .delete(name="cookedBooks", controller="cookedBooks", action="delete")\n\n    // Route name:  logout\n    // Example URL: /logout\n    // Controller:  Sessions\n    // Action:      delete\n    .delete(name="logout", to="sessions##delete")\n\n    // Route name:  clientsStatus\n    // Example URL: /statuses/4918\n    // Controller:  clients.Statuses\n    // Action:      delete\n    .delete(name="statuses", to="statuses##delete", package="clients")\n\n    // Route name:  blogComment\n    // Example URL: /comments/5432\n    // Controller:  blog.Comments\n    // Action:      delete\n    .delete(\n        name="comment",\n        pattern="comments/[key]",\n        to="comments##delete",\n        package="blog"\n    )\n.end();\n\n</cfscript>\n
"},"hint":"Create a route that matches a URL requiring an HTTP DELETE method. We recommend using this matcher to expose actions that delete database records.\n\n","returntype":"struct","slug":"mapper.delete","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"delete","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete a single object\n<cfscript>\npost = model(\"post\").findByKey(33);\nsuccess = post.delete();\n</cfscript>\n\nDeletes the post with ID 33 from the database.\n\nReturns true if deletion succeeds.\n\nExample 2: Scoped delete via association\n<cfscript>\npost = model(\"post\").findByKey(params.postId);\ncomment = model(\"comment\").findByKey(params.commentId);\n\n// Calls comment.delete() internally\npost.deleteComment(comment);\n</cfscript>\n\nIf post has a hasMany association to comment, this uses the association method to delete a related comment.\n\nExample 3: Permanent deletion (bypass soft-delete)\n<cfscript>\npost = model(\"post\").findByKey(33);\npost.delete(softDelete=false);\n</cfscript>\n\nForces a hard delete even if the model uses soft-delete columns.
"},"hint":"Deletes the object, which means the row is deleted from the database (unless prevented by a beforeDelete callback).\nReturns true on successful deletion of the row, false otherwise.\n\n","returntype":"boolean","slug":"model.delete","parameters":[{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"delete","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete inactive users (skip callbacks and validations)\n<cfscript>\nrecordsDeleted = model(\"user\").deleteAll(where=\"inactive=1\", instantiate=false);\nwriteOutput(\"Deleted #recordsDeleted# inactive users.\");\n</cfscript>\n\nDeletes all users where inactive=1.\n\nObjects are not instantiated, so callbacks and validations are skipped.\n\nExample 2: Scoped delete using an association\n<cfscript>\npost = model(\"post\").findByKey(params.postId);\n\n// Deletes all comments associated with this post\nhowManyDeleted = post.deleteAllComments();\nwriteOutput(\"Deleted #howManyDeleted# comments for this post.\");\n</cfscript>\n\nAssumes a hasMany association from post → comment.\n\nInternally calls model(\"comment\").deleteAll(where=\"postId=#post.id#\").\n\nExample 3: Delete and run callbacks\n<cfscript>\nrecordsDeleted = model(\"user\").deleteAll(where=\"inactive=1\", instantiate=true, callbacks=true);\n</cfscript>\n\nDeletes the records after instantiating the objects.\n\nAny beforeDelete or afterDelete callbacks are triggered.
"},"hint":"Deletes all records that match the where argument.\nBy default, objects will not be instantiated and therefore callbacks and validations are not invoked.\nYou can change this behavior by passing in instantiate=true.\nReturns the number of records that were deleted.\n\n","returntype":"numeric","slug":"model.deleteAll","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Whether or not to instantiate the object(s) first. When objects are not instantiated, any callbacks and validations set on them will be skipped.","name":"instantiate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"deleteAll","tags":{"categoryClass":"deletefunctions","sectionClass":"modelclass","category":"Delete Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete a user by primary key\n<cfscript>\nresult = model(\"user\").deleteByKey(1);\n\nif (result) {\n    writeOutput(\"User deleted successfully.\");\n} else {\n    writeOutput(\"Failed to delete user.\");\n}\n</cfscript>\n\nDeletes the user with id=1.\n\nReturns true on success, false on failure.\n\nExample 2: Delete a record permanently (ignore soft delete)\n<cfscript>\nresult = model(\"user\").deleteByKey(1, softDelete=false);\n\nif (result) {\n    writeOutput(\"User permanently deleted.\");\n}\n</cfscript>\n\nIgnores any soft delete column.\n\nThe record is removed from the database entirely.\n\nExample 3: Disable callbacks\n<cfscript>\nresult = model(\"user\").deleteByKey(1, callbacks=false);\n\nwriteOutput(\"Deleted user without triggering callbacks: #result#\");\n</cfscript>\n\nSkips any beforeDelete or afterDelete logic.
"},"hint":"Finds the record with the supplied key and deletes it.\nReturns true on successful deletion of the row, false otherwise.\n\n","returntype":"boolean","slug":"model.deleteByKey","parameters":[{"required":true,"hint":"Primary key value(s) of the record to fetch. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"deleteByKey","tags":{"categoryClass":"deletefunctions","sectionClass":"modelclass","category":"Delete Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Delete the most recently signed-up user\n<cfscript>\nresult = model(\"user\").deleteOne(order=\"signupDate DESC\");\n\nif (result) {\n    writeOutput(\"Deleted the most recently signed-up user.\");\n} else {\n    writeOutput(\"No user found to delete.\");\n}\n</cfscript>\n\nDeletes one record based on the order of signupDate descending.\n\nOnly the first matching record is deleted.\n\nExample 2: Delete a specific user by condition\n<cfscript>\nresult = model(\"user\").deleteOne(where=\"email='test@example.com'\");\n\nwriteOutput(\"Deletion status: #result#\");\n</cfscript>\n\nFinds a user with the email test@example.com and deletes it.\n\nExample 3: Scoped delete via association\n<cfscript>\n// Assuming a hasOne association: user -> profile\naUser = model(\"user\").findByKey(params.userId);\naUser.deleteProfile(); // deletes the profile associated with this user\n</cfscript>\n\ndeleteProfile() internally calls model(\"profile\").deleteOne(where=\"userId=#aUser.id#\").\n
"},"hint":"Finds a single record based on conditions and deletes it. Returns true if deletion succeeds, false otherwise. It is useful when you want to remove one specific record without fetching it manually first.\n\n","returntype":"boolean","slug":"model.deleteOne","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to permanently delete a record, even if it has a soft delete column.","name":"softDelete","type":"boolean"}],"availableIn":["model"],"name":"deleteOne","tags":{"categoryClass":"deletefunctions","sectionClass":"modelclass","category":"Delete Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Deobfuscate a single value\n<cfscript>\n// Assume \"b7ab9a50\" is an obfuscated ID\noriginalValue = deobfuscateParam(\"b7ab9a50\");\n\nwriteOutput(\"Original value: #originalValue#\");\n</cfscript>\n\nConverts the obfuscated string \"b7ab9a50\" back to its original value.\n\nUseful for safely passing IDs in URLs or forms while preventing direct exposure of database keys.\n\nExample 2: Deobfuscate a request parameter\n<cfscript>\n// Assume params.userId contains an obfuscated user ID\nuserId = deobfuscateParam(params.userId);\n\nuser = model(\"user\").findByKey(userId);\nwriteDump(user);\n</cfscript>\n\nSafely retrieves a user using an obfuscated ID passed in a URL or form.
"},"hint":"Converts an obfuscated string back into its original value. This is typically used when IDs or other sensitive data are encoded for security purposes and need to be restored to their original form.\n\n","returntype":"string","slug":"controller.deobfuscateParam","parameters":[{"required":true,"hint":"The value to deobfuscate.","name":"param","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"deobfuscateParam","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1: Basic usage\n<cfscript>\nrightNow = now();\naWhileAgo = dateAdd(\"d\", -30, rightNow);\n\ntimeDifference = distanceOfTimeInWords(aWhileAgo, rightNow);\nwriteOutput(timeDifference); // Outputs: \"about 1 month\"\n</cfscript>\n\nCalculates the difference between two dates.\n\nReturns \"about 1 month\" because aWhileAgo is 30 days before rightNow.\n\nExample 2: Include seconds\n<cfscript>\nstartTime = now();\nendTime = dateAdd(\"s\", 45, startTime);\n\ntimeDifference = distanceOfTimeInWords(startTime, endTime, true);\nwriteOutput(timeDifference); // Outputs: \"less than a minute\" or \"45 seconds\" depending on Wheels version\n</cfscript>\n\nUseful when you need a more precise human-readable difference for very short intervals.\n\nExample 3: Past vs future dates\n<cfscript>\npastDate = dateAdd(\"d\", -10, now());\nfutureDate = dateAdd(\"d\", 5, now());\n\nwriteOutput(distanceOfTimeInWords(pastDate, now()));   // \"10 days\"\nwriteOutput(distanceOfTimeInWords(now(), futureDate)); // \"5 days\"\n</cfscript>\n\nWorks regardless of the order of the dates.\n\nAlways returns a human-friendly description.
"},"hint":"Pass in two dates to this method, and it will return a string describing the difference between them.\n\n","returntype":"string","slug":"controller.distanceOfTimeInWords","parameters":[{"required":true,"hint":"Date to compare from.","name":"fromTime","type":"date"},{"required":true,"hint":"Date to compare to.","name":"toTime","type":"date"},{"default":false,"required":false,"hint":"Whether or not to include the number of seconds in the returned string.","name":"includeSeconds","type":"boolean"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"distanceOfTimeInWords","tags":{"categoryClass":"datefunctions","sectionClass":"globalhelpers","category":"Date Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
function down() {\n\ttransaction {\n\t\ttry {\n\t\t\t// your code goes here\n\t\t\tdropTable('myTable');\n\t\t} catch (any e) {\n\t\t\tlocal.exception = e;\n\t\t}\n\n\t\tif (StructKeyExists(local, "exception")) {\n\t\t\ttransaction action="rollback";\n\t\t\tthrow(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any");\n\t\t} else {\n\t\t\ttransaction action="commit";\n\t\t}\n\t}\n}\n
"},"hint":"down() defines the steps to revert a database migration. It’s executed when rolling back a migration, typically to undo the changes applied by the corresponding up() function. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.down","parameters":[],"availableIn":["migration"],"name":"down","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function up() {\n    // Remove a foreign key from the orders table\n    dropForeignKey(\n        table=\"orders\",\n        keyName=\"fk_orders_customerId\"\n    );\n}\n\ntable = \"orders\" -> the table that has the foreign key.\n\nkeyName = \"fk_orders_customerId\" -> the exact name of the foreign key constraint you want to drop.
"},"hint":"dropForeignKey() is used to remove a foreign key constraint from a table in the database. This is typically done during schema changes in migrations. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.dropForeignKey","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"the name of the key to drop","name":"keyName","type":"string"}],"availableIn":["migration"],"name":"dropForeignKey","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function up() {\n    // Remove a foreign key reference from the orders table\n    dropReference(\n        table=\"orders\",\n        referenceName=\"customer_ref\"\n    );\n}\n\ntable = \"orders\" -> the table that contains the foreign key reference.\n\nreferenceName = \"customer_ref\" -> the reference name that was originally defined when the foreign key was created.
"},"hint":"dropReference() is used to remove a foreign key constraint from a table in the database using the reference name that was originally used to create it. This is slightly different from dropForeignKey(), which requires the actual key name. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.dropReference","parameters":[{"required":true,"hint":"The table name to perform the operation on","name":"table","type":"string"},{"required":true,"hint":"the name of the reference to drop","name":"referenceName","type":"string"}],"availableIn":["migration"],"name":"dropReference","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function down() {\n    // Drop the 'users' table\n    dropTable(name=\"users\");\n}\n\nname = \"users\" -> the table that you want to remove from the database.\n\nNotes\n\nTypically used in the down() method of a migration when rolling back a previous createTable().\n\nCan be combined with transaction {} to ensure rollback in case of errors:\n\nfunction down() {\n    transaction {\n        try {\n            dropTable(\"orders\");\n        } catch (any e) {\n            transaction action=\"rollback\";\n            throw(errorCode=\"1\", detail=e.detail, message=e.message, type=\"any\");\n        }\n        transaction action=\"commit\";\n    }\n}\n\nCaution: This operation permanently deletes all data in the table.
"},"hint":"dropTable() is used to remove a table from the database entirely. This is a destructive operation, so all data in the table will be lost. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.dropTable","parameters":[{"required":true,"hint":"Name of the table to drop","name":"name","type":"string"}],"availableIn":["migration"],"name":"dropTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function down() {\n    // Drop the 'active_users' view\n    dropView(name=\"active_users\");\n}\n\nname = \"active_users\" -> the view that you want to remove from the database.\n\nNotes\n\nTypically used in the down() method of a migration when rolling back a previous createView().\n\nCan be wrapped in a transaction for safety:\n\nfunction down() {\n    transaction {\n        try {\n            dropView(\"recent_orders\");\n        } catch (any e) {\n            transaction action=\"rollback\";\n            throw(errorCode=\"1\", detail=e.detail, message=e.message, type=\"any\");\n        }\n        transaction action=\"commit\";\n    }\n}\n\nCaution: This permanently deletes the view definition. Any queries depending on the view will fail after this operation.
"},"hint":"dropView() is used to remove a database view entirely. A view is a saved query that acts like a virtual table, so this operation deletes that virtual table definition. Only available in a migration CFC\n\n","returntype":"void","slug":"migration.dropView","parameters":[{"required":true,"hint":"Name of the view to drop","name":"name","type":"string"}],"availableIn":["migration"],"name":"dropView","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    .namespace("admin")\n        .resources("products")\n    .end() // Ends the `namespace` block.\n\n    .scope(package="public")\n        .resources(name="products", nested=true)\n          .resources("variations")\n        .end() // Ends the nested `resources` block.\n    .end() // Ends the `scope` block.\n.end(); // Ends the `mapper` block.\n\n</cfscript>
"},"hint":"Call this to end a nested routing block or the entire route configuration. This method is chained on a sequence of routing mapper method calls started by mapper().\n\n","returntype":"struct","slug":"mapper.end","parameters":[],"availableIn":["mapper"],"name":"end","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
#startFormTag(action="create")#\n <input type="text" name="firstName" placeholder="First Name">\n <input type="text" name="lastName" placeholder="Last Name">\n#endFormTag()#\n\nOutput:\n<form action="/create" method="post">\n <input type="text" name="firstName" placeholder="First Name">\n <input type="text" name="lastName" placeholder="Last Name">\n</form>\n\n#startFormTag(action="update")#\n <input type="email" name="email" placeholder="Email">\n#endFormTag(prepend="<div class='form-wrapper'>", append="</div>")#\n\nOutput:\n<div class='form-wrapper'>\n<form action="/update" method="post">\n <input type="email" name="email" placeholder="Email">\n</form>\n</div>\n\n#startFormTag(action="login")#\n <input type="text" name="username">\n#endFormTag(encode=true)#\n\nOutput:\n<form action="/login" method="post">\n <input type="text" name="username">\n</form>\n\n#startFormTag(action="register", prepend="<section>")#\n <input type="text" name="username">\n <input type="password" name="password">\n#endFormTag(append="</section>")#\n\nOutput:\n<section>\n<form action="/register" method="post">\n <input type="text" name="username">\n <input type="password" name="password">\n</form>\n</section>\n
"},"hint":"Builds and returns a string containing the closing form tag. It’s typically used in conjunction with startFormTag().\n\n","returntype":"string","slug":"controller.endFormTag","parameters":[{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"endFormTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Check how many errors are set on the object\nif(author.errorCount() GTE 10){\n    // Do something to deal with this very erroneous author here...\n}\n\n2. Check how many errors are associated with the `email` property\nif(author.errorCount("email") gt 0){\n    // Do something to deal with this erroneous author here...\n}\n\n3. Count errors by error name\nif (author.errorCount(\"\", \"invalidFormat\") GT 0) {\n    // Handle errors with a specific error name\n    writeOutput(\"There are fields with invalid formatting!\");\n}
"},"hint":"Returns the number of errors this object has associated with it.\nSpecify property or name if you wish to count only specific errors.\n\n","returntype":"numeric","slug":"model.errorCount","parameters":[{"default":"","required":false,"hint":"Specify a property name here if you want to count only errors set on a specific property.","name":"property","type":"string"},{"default":"","required":false,"hint":"Specify an error name here if you want to count only errors set with a specific error name.","name":"name","type":"string"}],"availableIn":["model"],"name":"errorCount","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfoutput>\n#errorMessageOn(objectName="author", property="email")#\n</cfoutput>\n\nDisplays the first error message for the email property of the author object.\n\nDefault wrapper is <span class="error-message">.\n\nExample 2 — Custom wrapper and class\n<cfoutput>\n#errorMessageOn(\n objectName="author",\n property="email",\n wrapperElement="div",\n class="alert alert-danger"\n)#\n</cfoutput>\n\nWraps the error in a <div> instead of <span>.\n\nUses Bootstrap classes for styling.\n\nExample 3 — Prepend or append text\n<cfoutput>\n#errorMessageOn(\n objectName="author",\n property="email",\n prependText="Error: ",\n appendText=" Please fix it."\n)#\n</cfoutput>\n\nPrepends "Error: " and appends " Please fix it." around the actual error message.\n\nExample 4 — With HTML encoding disabled\n<cfoutput>\n#errorMessageOn(\n objectName="author",\n property="email",\n encode=false\n)#\n</cfoutput>\n\nOutput is not encoded, which can be useful if you want to include HTML formatting inside the error message itself.\n
"},"hint":"Returns the error message, if one exists, on the object's property.\nIf multiple error messages exist, the first one is returned. If no error exists, it returns an empty string.\n\n","returntype":"string","slug":"controller.errorMessageOn","parameters":[{"required":true,"hint":"The variable name of the object to display the error message for.","name":"objectName","type":"string"},{"required":true,"hint":"The name of the property to display the error message for.","name":"property","type":"string"},{"default":"","required":false,"hint":"String to prepend to the error message.","name":"prependText","type":"string"},{"default":"","required":false,"hint":"String to append to the error message.","name":"appendText","type":"string"},{"default":"span","required":false,"hint":"HTML element to wrap the error message in.","name":"wrapperElement","type":"string"},{"default":"error-message","required":false,"hint":"CSS `class` to set on the wrapper element.","name":"class","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"errorMessageOn","tags":{"categoryClass":"errorfunctions","sectionClass":"viewhelpers","category":"Error Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfoutput>\n#errorMessagesFor(objectName="author")#\n</cfoutput>\n\nGenerates a <ul class="error-messages"> containing all errors for the author object.\n\nDefault behavior includes all associated object errors.\n\nExample 2 — Custom CSS class\n<cfoutput>\n#errorMessagesFor(objectName="author", class="alert alert-danger")#\n</cfoutput>\n\nUses a custom CSS class for styling (e.g., Bootstrap alerts).\n\nExample 3 — Exclude duplicate errors\n<cfoutput>\n#errorMessagesFor(objectName="author", showDuplicates=false)#\n</cfoutput>\n\nPrevents duplicate messages from appearing multiple times in the list.\n\nExample 4 — Include or exclude associated objects\n<cfoutput>\n<!--- Only show errors on this object, not on associated objects --->\n#errorMessagesFor(objectName="author", includeAssociations=false)#\n</cfoutput>\n\nUseful if you want to display errors for the main object separately from associated objects (like a nested profile or address).\n\nExample 5 — HTML encoding disabled\n<cfoutput>\n#errorMessagesFor(objectName="author", encode=false)#\n</cfoutput>\n\nErrors are output as-is, allowing embedded HTML in the messages (use with caution).\n
"},"hint":"Builds and returns a list (ul tag with a default class of error-messages) containing all the error messages for all the properties of the object.\nReturns an empty string if no errors exist.\n\n","returntype":"string","slug":"controller.errorMessagesFor","parameters":[{"required":true,"hint":"The variable name of the object to display error messages for.","name":"objectName","type":"string"},{"default":"error-messages","required":false,"hint":"CSS `class` to set on the `ul` element.","name":"class","type":"string"},{"default":true,"required":false,"hint":"Whether or not to show duplicate error messages.","name":"showDuplicates","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"},{"default":true,"required":false,"name":"includeAssociations","type":"boolean"}],"availableIn":["controller"],"name":"errorMessagesFor","tags":{"categoryClass":"errorfunctions","sectionClass":"viewhelpers","category":"Error Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfscript>\nuser = model("user").findByKey(12);\n\nerrors = user.errorsOn("emailAddress");\n\nwriteDump(errors);\n</cfscript>\n\nReturns an array of error objects associated with the emailAddress property.\n\nEach element typically contains the error message and metadata like name or type.\n\nExample 2 — Filter by error name\n<cfscript>\nerrors = user.errorsOn("emailAddress", "uniqueEmail");\n\nwriteDump(errors);\n</cfscript>\n\nReturns only errors for emailAddress that have the error name uniqueEmail.\n\nExample 3 — Checking if a property has any errors\n<cfscript>\nif (arrayLen(user.errorsOn("password")) > 0) {\n writeOutput("Password has errors!");\n}\n</cfscript>\n\nThis is helpful when you need conditional logic based on whether a field has errors.\n\nExample 4 — Iterating over errors\n<cfscript>\nerrors = user.errorsOn("username");\n\nfor (var e in errors) {\n writeOutput("Error: " & e.message & "<br>");\n}\n</cfscript>\n\nLoops through all errors on a property and outputs the messages individually.\n
"},"hint":"errorsOn() returns an array of all errors associated with a specific property of a model object. You can also filter by a specific error name if needed. This is useful when you need programmatic access to errors rather than just displaying them in the view.\n\n","returntype":"array","slug":"model.errorsOn","parameters":[{"required":true,"hint":"Specify the property name to return errors for here.","name":"property","type":"string"},{"default":"","required":false,"hint":"If you want to return only errors on the property set with a specific error name you can specify it here.","name":"name","type":"string"}],"availableIn":["model"],"name":"errorsOn","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Get all base errors\n<cfscript>\nuser = model("user").findByKey(12);\n\nerrors = user.errorsOnBase();\n\nwriteDump(errors);\n</cfscript>\n\nReturns all general errors on the user object.\n\nEach element typically contains message, name, and type information.\n\nExample 2 — Filter by error name\n<cfscript>\nerrors = user.errorsOnBase("accountLocked");\n\nwriteDump(errors);\n</cfscript>\n\nReturns only base errors that have the error name accountLocked.\n\nExample 3 — Conditional logic with base errors\n<cfscript>\nif (arrayLen(user.errorsOnBase()) > 0) {\n writeOutput("There are general errors on this user account.");\n}\n</cfscript>\n\nThis can be used to block actions or display notices when object-level errors exist.\n\nExample 4 — Iterating over base errors\n<cfscript>\nfor (var e in user.errorsOnBase()) {\n writeOutput("General error: " & e.message & "<br>");\n}\n</cfscript>\n\nLoops through each object-level error and outputs its message.\n
"},"hint":"errorsOnBase() returns an array of all errors associated with the object as a whole, not tied to any specific property. This is useful for general errors such as system-level validations, cross-field validations, or custom errors added at the object level.\n\n","returntype":"array","slug":"model.errorsOnBase","parameters":[{"default":"","required":false,"hint":"Specify an error name here to only return errors for that error name.","name":"name","type":"string"}],"availableIn":["model"],"name":"errorsOnBase","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
Example 1 — Basic usage\n<cfscript>\ntext = "Wheels is a Rails-like MVC framework for Adobe ColdFusion and Lucee";\n\nsnippet = excerpt(text=text, phrase="framework", radius=5);\n\nwriteOutput(snippet);\n</cfscript>\n\nOutput:\n\n... MVC framework for ...\n\nExtracts 5 characters before and after "framework".\n\nAdds ... at the start and end to indicate truncation.\n\nExample 2 — Increase radius\n<cfscript>\nsnippet = excerpt(text=text, phrase="framework", radius=20);\n\nwriteOutput(snippet);\n</cfscript>\n\nOutput:\n\n... Rails-like MVC framework for Adobe Cold...\n\nShows more surrounding context (20 characters before and after the phrase).\n\nExample 3 — Custom excerpt string\n<cfscript>\nsnippet = excerpt(\n text=text,\n phrase="framework",\n radius=10,\n excerptString="***"\n);\n\nwriteOutput(snippet);\n</cfscript>\n\nOutput:\n\n*** Rails-like MVC framework for Adob ***\n\nUses *** instead of ... to mark truncated text.\n
"},"hint":"excerpt() extracts a portion of text surrounding the first instance of a given phrase. This is useful for previews, search result snippets, or highlighting context around a keyword.\n\n","returntype":"string","slug":"controller.excerpt","parameters":[{"required":true,"hint":"The text to extract an excerpt from.","name":"text","type":"string"},{"required":true,"hint":"The phrase to extract.","name":"phrase","type":"string"},{"default":100,"required":false,"hint":"Number of characters to extract surrounding the phrase.","name":"radius","type":"numeric"},{"default":"...","required":false,"hint":"String to replace first and / or last characters with.","name":"excerptString","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"excerpt","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\nfunction up() {\n  transaction {\n     // Execute a raw SQL statement\n     execute(sql="INSERT INTO users (firstname, lastname, email) VALUES ('John', 'Doe', 'john@example.com')");\n  }\n}\n</cfscript>\n
"},"hint":"execute() allows you to run a raw SQL query directly from a migration file. This is useful when you need to perform operations that aren’t easily handled by the built-in migration methods like createTable() or addColumn(). Only available in a migration CFC\n\n","returntype":"void","slug":"migration.execute","parameters":[{"required":true,"hint":"Arbitary SQL String","name":"sql","type":"string"}],"availableIn":["migration"],"name":"execute","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Checking if Joe exists in the database\nresult = model("user").exists(where="firstName = 'Joe'");\n\n2. Checking if a specific user exists based on a primary key valued passed in through the URL/form in an if statement\nif (model("user").exists(keyparams.key))\n{\n\t// Do something\n}\n\n3. If you have a `belongsTo` association setup from `comment` to `post`, you can do a scoped call. (The `hasPost` method below will call `model("post").exists(comment.postId)` internally.)\ncomment = model("comment").findByKey(params.commentId);\ncommentHasAPost = comment.hasPost();\n\n4. If you have a `hasOne` association setup from `user` to `profile`, you can do a scoped call. (The `hasProfile` method below will call `model("profile").exists(where="userId=#user.id#")` internally.)\nuser = model("user").findByKey(params.userId);\nuserHasProfile = user.hasProfile();\n\n5. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `hasComments` method below will call `model("comment").exists(where="postid=#post.id#")` internally.)\npost = model("post").findByKey(params.postId);\npostHasComments = post.hasComments();
"},"hint":"Checks if a record exists in the table.\nYou can pass in either a primary key value to the key argument or a string to the where argument.\nIf you don't pass in either of those, it will simply check if any record exists in the table.\n\n","returntype":"boolean","slug":"model.exists","parameters":[{"required":false,"hint":"Primary key value(s) of the record. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"exists","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Simple fail with no message\nfunction test_should_fail_on_purpose() {\n fail();\n};\n\nMarks the test as failed without explanation.\n\n2. Fail with a custom message\nfunction test_should_fail_with_a_message() {\n fail("This path should never be reached!");\n};\n\nProduces a failure with the message This path should never be reached!.\n\n3. Guarding unexpected conditions\nfunction test_should_not_allow_null_users() {\n var user = getUserById(123);\n if (isNull(user)) {\n     fail("Expected user with ID 123 to exist but got null.");\n }\n};\n
"},"hint":"Forces a test to fail intentionally. You can call fail() inside a test when you want to stop execution and explicitly mark the test as failed or highlight cases that should never happen. When called, it throws an exception that results in a test failure. You can optionally pass a custom message to clarify why the failure occurred. Used in wheels legacy testing.\n\n","returntype":"void","slug":"test.fail","parameters":[{"default":"","required":false,"name":"message","type":"string"}],"availableIn":["test"],"name":"fail","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Provide a `label` and the required `objectName` and `property`\n#fileField(label="Photo", objectName="photo", property="imageFile")#\n\n\n2. Display fields for photos provided by the `screenshots` association and nested properties\n<fieldset>\n\t<legend>Screenshots</legend>\n\t<cfloop from="1" to="#ArrayLen(site.screenshots)#" index="i">\n\t\t#fileField(label="File ##i#", objectName="site", association="screenshots", position=i, property="file")#\n\t\t#textField(label="Caption ##i#", objectName="site", association="screenshots", position=i, property="caption")#\n\t</cfloop>\n</fieldset>\n
"},"hint":"Builds and returns a string containing a file field form control based on the supplied objectName and property.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.fileField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"fileField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with label\n#fileFieldTag(label="Upload Photo", name="photo")#\n\nOutput:\n\n<label for="photo">Upload Photo</label>\n<input type="file" id="photo" name="photo">\n\n2. With custom attributes\n#fileFieldTag(\n label="Resume", \n name="resume", \n class="upload", \n id="resume-upload", \n accept=".pdf,.docx"\n)#\n\nAdds CSS class, ID, and file type restrictions.\n\n3. Label placement options\n#fileFieldTag(label="Avatar", name="avatar", labelPlacement="before")#\n#fileFieldTag(label="Attachment", name="attachment", labelPlacement="after")#\n\nMoves the label before or after the <input> instead of wrapping.\n\n4. Prepending/Appending markup\n#fileFieldTag(\n label="Select File", \n name="document", \n prepend='<div class="field-wrapper">', \n append='</div>'\n)#\n\nWraps the input inside a custom <div>.\n\n5. Label customization with prepend/append\n#fileFieldTag(\n label="Profile Photo", \n name="profile", \n prependToLabel='<span class="required">*</span>', \n appendToLabel=' <small>(max 2MB)</small>'\n)#\n
"},"hint":"Builds and returns a string containing a file form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.fileFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"fileFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Get filter chain.\nmyFilterChain = filterChain();\n\n2. Get filter chain for after filters only.\nmyFilterChain = filterChain(type="after");\n
"},"hint":"The filterChain() function returns an array of all filters that are set on the current controller in the order they will be executed. By default, it includes both before and after filters, but you can specify the type argument if you want to return only one type. For example, setting type=\"after\" will return only the filters that run after the controller action.\n\n","returntype":"array","slug":"controller.filterChain","parameters":[{"default":"all","required":false,"hint":"Use this argument to return only before or after filters.","name":"type","type":"string"}],"availableIn":["controller"],"name":"filterChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Run a filter before all actions\n// Always execute restrictAccess before every action\nfilters("restrictAccess");\n\n2. Multiple filters before all actions\n// Run both isLoggedIn and checkIPAddress before all actions\nfilters(through="isLoggedIn, checkIPAddress");\n\n3. Exclude specific actions\n// Run filters before all actions, except home and login\nfilters(through="isLoggedIn, checkIPAddress", except="home, login");\n\n4. Limit filters to specific actions\n// Only run ensureAdmin before the delete action\nfilters(through="ensureAdmin", only="delete");\n\n5. Run filters after an action\n// Run logAction after every action\nfilters(through="logAction", type="after");\n
"},"hint":"The filters() function lets you specify methods in your controller that should run automatically either before or after certain actions. Filters are useful for handling cross-cutting concerns such as authentication, authorization, logging, or cleanup, without having to repeat the same code inside each action. By default, filters run before the action, but you can configure them to run after, limit them to specific actions, exclude them from others, or control their placement in the filter chain.\n\n","returntype":"void","slug":"controller.filters","parameters":[{"required":true,"hint":"Function(s) to execute before or after the action(s).","name":"through","type":"string"},{"default":"before","required":false,"hint":"Whether to run the function(s) before or after the action(s).","name":"type","type":"string"},{"default":"","required":false,"hint":"Pass in a list of action names (or one action name) to tell Wheels that the filter function(s) should only be run on these actions.","name":"only","type":"string"},{"default":"","required":false,"hint":"Pass in a list of action names (or one action name) to tell Wheels that the filter function(s) should be run on all actions except the specified ones.","name":"except","type":"string"},{"default":"append","required":false,"hint":"Pass in `prepend` to prepend the function(s) to the filter chain instead of appending.","name":"placement","type":"string"}],"availableIn":["controller"],"name":"filters","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Getting only 5 users and ordering them randomly\nfiveRandomUsers = model("user").findAll(maxRows=5, order="random");\n\n2. Including an association (which in this case needs to be setup as a `belongsTo` association to `author` on the `article` model first)\narticles = model("article").findAll( include="author", where="published=1", order="createdAt DESC" );\n\n3. Similar to the above but using the association in the opposite direction (which needs to be setup as a `hasMany` association to `article` on the `author` model)\nbobsArticles = model("author").findAll( include="articles", where="firstName='Bob'" );\n\n4. Using pagination (getting records 26-50 in this case) and a more complex way to include associations (a song `belongsTo` an album, which in turn `belongsTo` an artist)\nsongs = model("song").findAll( include="album(artist)", page=2, perPage=25 );\n\n5. Using a dynamic finder to get all books released a certain year. Same as calling model("book").findOne(where="releaseYear=#params.year#")\nbooks = model("book").findAllByReleaseYear(params.year);\n\n6. Getting all books of a certain type from a specific year by using a dynamic finder. Same as calling  model("book").findAll( where="releaseYear=#params.year# AND type='#params.type#'" )\nbooks = model("book").findAllByReleaseYearAndType( "#params.year#,#params.type#" );\n\n7. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `comments` method below will call `model("comment").findAll(where="postId=#post.id#")` internally)\npost = model("post").findByKey(params.postId);\ncomments = post.comments();\n\n8. If you have an `Order` model with properties for `productId`, `amount` and a calculated property named `totalAmount` (set up as `property(name="totalAmount", sql="SUM(amount)")`), then you can do the following to get the ids for all products with over $1,000 in sales (the SQL will be created using `HAVING` instead of `WHERE` in this case since you're getting an aggregate value for a calculated property)\nids = model("order").findAll(group="productId", where="totalAmount > 1000");\n\n9. Using index hints\nindexes = {\n\tauthor="idx_authors_123",\n\tpost="idx_posts_123"\n}\nposts = model("author").findAll(where="firstname LIKE '#params.q#%' OR subject LIKE '#params.q#%'", include="posts", useIndex=indexes);\n
"},"hint":"Returns records from the database table mapped to this model according to the arguments passed in (use the where argument to decide which records to get, use the order argument to set the order in which those records should be returned, and so on).\nThe records will be returned as either a cfquery result set, an array of objects, or an array of structs (depending on what the returnAs argument is set to).\n\n","returntype":"any","slug":"model.findAll","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":"","required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"},{"default":"","required":false,"hint":"Determines how the `SELECT` clause for the query used to return data will look. You can pass in a list of the properties (which map to columns) that you want returned from your table(s). If you don't set this argument at all, Wheels will select all properties from your table(s). If you specify a table name (e.g. `users.email`) or alias a column (e.g. `fn AS firstName`) in the list, then the entire list will be passed through unchanged and used in the `SELECT` clause of the query. By default, all column names in tables joined via the `include` argument will be prepended with the singular version of the included table name.","name":"select","type":"string"},{"default":"false","required":false,"hint":"Whether to add the `DISTINCT` keyword to your `SELECT` clause. Wheels will, when necessary, add this automatically (when using pagination and a `hasMany` association is used in the `include` argument, to name one example).","name":"distinct","type":"boolean"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"-1","required":false,"hint":"Maximum number of records to retrieve. Passed on to the `maxRows` `cfquery` attribute. The default, `-1`, means that all records will be retrieved.","name":"maxRows","type":"numeric"},{"default":"0","required":false,"hint":"If you want to paginate records, you can do so by specifying a page number here. For example, getting records 11-20 would be page number 2 when `perPage` is kept at the default setting (10 records per page). The default, 0, means that records won't be paginated and that the `perPage` and `count` arguments will be ignored.","name":"page","type":"numeric"},{"default":10,"required":false,"hint":"When using pagination, you can specify how many records you want to fetch per page here. This argument is only used when the `page` argument has been passed in.","name":"perPage","type":"numeric"},{"default":"0","required":false,"hint":"When using pagination and you know in advance how many records you want to paginate through, you can pass in that value here. Doing so will prevent Wheels from running a `COUNT` query to get this value. This argument is only used when the `page` argument has been passed in.","name":"count","type":"numeric"},{"default":"query","required":false,"hint":"Handle to use for the query. This is used when you're paginating multiple queries and need to reference them individually in the `paginationLinks()` function. It's also used to set the name of the query in the debug output (which otherwise defaults to `userFindAllQuery` for example).","name":"handle","type":"string"},{"default":"","required":false,"hint":"If you want to cache the query, you can do so by specifying the number of minutes you want to cache the query for here. If you set it to `true`, the default cache time will be used (60 minutes).","name":"cache","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"query","required":false,"hint":"Set to `objects` to return an array of objects, set to `structs` to return a struct of structs, set to `array` to return an array of structs, set to `query` to return a query result set, or set to 'sql' to return the executed SQL query as a string.","name":"returnAs","type":"string"},{"default":true,"required":false,"hint":"When `returnAs` is set to `objects`, you can set this argument to `false` to prevent returning objects fetched from associations specified in the `include` argument. This is useful when you only need to include associations for use in the `WHERE` clause only and want to avoid the performance hit that comes with object creation.","name":"returnIncluded","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"[runtime expression]","required":false,"hint":"Override the default datasource","name":"dataSource","type":"string"},{"default":"0","required":false,"name":"$limit","type":"numeric"},{"default":"0","required":false,"name":"$offset","type":"numeric"}],"availableIn":["model"],"name":"findAll","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get all IDs for a model (basic usage):\n\nartistIds = model("artist").findAllKeys();\n\nReturns a comma-delimited list of all artist IDs.\n\n2. Get active artist IDs with custom delimiter and quotes:\n\nartistIds = model("artist").findAllKeys(quoted=true, delimiter="|", where="active=1");\n\nReturns only active artist IDs, quoted and separated with |.\n\n3. Limit results (top 10 user IDs):\n\nuserIds = model("user").findAllKeys(maxRows=10, order="createdAt DESC");\n\nReturns the 10 most recently created user IDs.\n\n4. Paginated IDs (books, second page):\n\nbookIds = model("book").findAllKeys(page=2, perPage=20, order="title ASC");\n\nFetches IDs for books on page 2 (records 21–40), ordered alphabetically.\n\n5. Grouped query with HAVING (order IDs by sales total):\n\norderIds = model("order").findAllKeys(group="productId", where="totalAmount > 500");\n\nReturns order IDs for products that generated more than $500 in sales.\n
"},"hint":"The findAllKeys() function retrieves all primary key values for a model’s records and returns them as a list. By default, the values are separated with commas, but you can change the delimiter with the delimiter argument or add single quotes around each value with the quoted argument. Since findAllKeys() accepts all arguments that findAll() does, you can also filter results with where, control ordering with order, or even include associations when filtering. This makes it useful when you need just the IDs of records without fetching full objects or rows.\n\n","returntype":"string","slug":"model.findAllKeys","parameters":[{"default":"false","required":false,"hint":"Set to `true` to enclose each value in single-quotation marks.","name":"quoted","type":"boolean"},{"default":",","required":false,"hint":"The delimiter character to separate the list items with.","name":"delimiter","type":"string"}],"availableIn":["model"],"name":"findAllKeys","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Getting the author with the primary key value `99` as an object\nauth = model("author").findByKey(99);\n\n2. Getting an author based on a form/URL value and then checking if it was found\nauth = model("author").findByKey(params.key);\nif(!isObject(auth)){\n    flashInsert(message="Author #params.key# was not found");\n    redirectTo(back=true);\n}\n\n3. If you have a `belongsTo` association setup from `comment` to `post`, you can do a scoped call. (The `post` method below will call `model("post").findByKey(comment.postId)` internally)\ncomment = model("comment").findByKey(params.commentId);\npost = comment.post();
"},"hint":"The findByKey() function retrieves a single record from the database using its primary key value and returns it as an object by default. If the record is not found, it returns false, making it easy to handle missing data gracefully. You can also control what columns are returned using the select argument, include related associations, or override the return format to a query, struct, or even raw SQL. Since it accepts the same options as other read functions like findOne(), you can apply caching, indexing, and even include soft-deleted records when needed.\n\n","returntype":"any","slug":"model.findByKey","parameters":[{"required":true,"hint":"Primary key value(s) of the record. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"default":"","required":false,"hint":"Determines how the `SELECT` clause for the query used to return data will look. You can pass in a list of the properties (which map to columns) that you want returned from your table(s). If you don't set this argument at all, Wheels will select all properties from your table(s). If you specify a table name (e.g. `users.email`) or alias a column (e.g. `fn AS firstName`) in the list, then the entire list will be passed through unchanged and used in the `SELECT` clause of the query. By default, all column names in tables joined via the `include` argument will be prepended with the singular version of the included table name.","name":"select","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"query","required":false,"hint":"Handle to use for the query. This is used to set the name of the query in the debug output (which otherwise defaults to `userFindOneQuery` for example).","name":"handle","type":"string"},{"default":"","required":false,"hint":"If you want to cache the query, you can do so by specifying the number of minutes you want to cache the query for here. If you set it to `true`, the default cache time will be used (60 minutes).","name":"cache","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"object","required":false,"hint":"Set to `objects` to return an array of objects, set to `structs` to return a struct of structs, set to `array` to return an array of structs, set to `query` to return a query result set, or set to 'sql' to return the executed SQL query as a string.","name":"returnAs","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Override the default datasource","name":"dataSource","type":"string"}],"availableIn":["model"],"name":"findByKey","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get the first record by primary key (default behavior):\n\nfirstUser = model("user").findFirst();\n\nFetches the user with the lowest primary key value.\n\n2. Get the first record alphabetically by name:\n\nfirstAuthor = model("author").findFirst(property="lastName");\n\nFetches the author with the alphabetically first last name.\n\n3. Get the earliest created record (using a timestamp column):\n\nfirstArticle = model("article").findFirst(property="createdAt");\n\nFetches the oldest article based on creation date.\n\n4. Get the cheapest product:\n\ncheapestProduct = model("product").findFirst(property="price");\n\nFetches the product with the lowest price.\n\n5. Use alias properties instead of property:\n\nfirstComment = model("comment").findFirst(properties="createdAt");\n\nWorks the same as property — useful when you prefer the plural alias.\n
"},"hint":"The findFirst() function fetches the first record from the database table mapped to the model, ordered by the primary key value by default. You can customize the ordering by passing a property name through the property argument, which is also aliased as properties. This makes it useful when you want the \"first\" record based on a specific field (e.g., earliest created date, alphabetically first name, lowest price, etc.). The result is returned as a model object.\n\n","returntype":"any","slug":"model.findFirst","parameters":[{"default":"[runtime expression]","required":false,"hint":"Name of the property to order by. This argument is also aliased as `properties`.","name":"property","type":"string"},{"default":"ASC","required":false,"name":"$sort","type":"string"}],"availableIn":["model"],"name":"findFirst","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get the last record by primary key (default behavior):\n\nlastUser = model("user").findLastOne();\n\nFetches the user with the highest primary key value.\n\n2. Get the last record alphabetically by name:\n\nlastAuthor = model("author").findLastOne(property="lastName");\n\nFetches the author with the alphabetically last last name.\n\n3. Get the most recently created record:\n\nlastArticle = model("article").findLastOne(property="createdAt");\n\nFetches the article with the latest creation date.\n\n4. Get the most expensive product:\n\npriciestProduct = model("product").findLastOne(property="price");\n\nFetches the product with the highest price.\n\n5. Use alias properties instead of property:\n\nlastComment = model("comment").findLastOne(properties="createdAt");\n\nWorks the same as property — useful when you prefer the plural alias.\n
"},"hint":"The findLastOne() function fetches the last record from the database table mapped to the model, ordered by the primary key value by default. You can override this ordering by passing a property name through the property argument (also aliased as properties). This is useful when you want to retrieve the \"last\" record based on something other than the primary key, such as the most recently created entry, the highest price, or the latest updated timestamp. The result is returned as a model object. This function was formerly known as findLast.\n\n","returntype":"any","slug":"model.findLastOne","parameters":[{"required":false,"hint":"Name of the property to order by. This argument is also aliased as `properties`.","name":"property","type":"string"}],"availableIn":["model"],"name":"findLastOne","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Getting the most recent order as an object from the database\norder = model("order").findOne(order="datePurchased DESC");\n\n2. Using a dynamic finder to get the first person with the last name `Smith`. Same as calling `model("user").findOne(where"lastName='Smith'")`\nperson = model("user").findOneByLastName("Smith");\n\n3. Getting a specific user using a dynamic finder. Same as calling `model("user").findOne(where"email='someone@somewhere.com' AND password='mypass'")`\nuser = model("user").findOneByEmailAndPassword("someone@somewhere.com,mypass");\n\n4. If you have a `hasOne` association setup from `user` to `profile`, you can do a scoped call. (The `profile` method below will call `model("profile").findOne(where="userId=#user.id#")` internally)\nuser = model("user").findByKey(params.userId);\nprofile = user.profile();\n\n5. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `findOneComment` method below will call `model("comment").findOne(where="postId=#post.id#")` internally)\npost = model("post").findByKey(params.postId);\ncomment = post.findOneComment(where="text='I Love Wheels!'");
"},"hint":"Fetches the first record found based on the WHERE and ORDER BY clauses.\nWith the default settings (i.e. the returnAs argument set to object), a model object will be returned if the record is found and the boolean value false if not.\nInstead of using the where argument, you can create cleaner code by making use of a concept called Dynamic Finders.\n\n","returntype":"any","slug":"model.findOne","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":"","required":false,"hint":"Determines how the `SELECT` clause for the query used to return data will look. You can pass in a list of the properties (which map to columns) that you want returned from your table(s). If you don't set this argument at all, Wheels will select all properties from your table(s). If you specify a table name (e.g. `users.email`) or alias a column (e.g. `fn AS firstName`) in the list, then the entire list will be passed through unchanged and used in the `SELECT` clause of the query. By default, all column names in tables joined via the `include` argument will be prepended with the singular version of the included table name.","name":"select","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"query","required":false,"hint":"Handle to use for the query. This is used to set the name of the query in the debug output (which otherwise defaults to `userFindOneQuery` for example).","name":"handle","type":"string"},{"default":"","required":false,"hint":"If you want to cache the query, you can do so by specifying the number of minutes you want to cache the query for here. If you set it to `true`, the default cache time will be used (60 minutes).","name":"cache","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"object","required":false,"hint":"Set to `objects` to return an array of objects, set to `structs` to return a struct of structs, set to `array` to return an array of structs, set to `query` to return a query result set, or set to 'sql' to return the executed SQL query as a string.","name":"returnAs","type":"string"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"[runtime expression]","required":false,"hint":"Override the default datasource","name":"dataSource","type":"string"}],"availableIn":["model"],"name":"findOne","tags":{"categoryClass":"readfunctions","sectionClass":"modelclass","category":"Read Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get a specific Flash value (commonly used for notifications or messages)\n\nnotice = flash("notice");\n\n2. Get another value stored in Flash, e.g., an error message\n\nerrorMsg = flash("error");\n\n3. Retrieve the entire Flash scope as a struct\n\nallFlash = flash();\n
"},"hint":"The flash() function is used in controllers to access data stored in the Flash scope. Flash is a temporary storage mechanism that lets you persist values across the next request (often after a redirect). You can use it to retrieve a specific key or the entire Flash struct. If you pass in a key, it returns the value associated with it; if no key is passed, it returns all the Flash contents as a struct.\n\n","returntype":"any","slug":"controller.flash","parameters":[{"required":false,"hint":"The key to get the value for.","name":"key","type":"string"}],"availableIn":["controller"],"name":"flash","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Clear all flash values at the start of an action\nflashClear();\n\n2. Clear messages after they've been displayed\nnotice = flash("notice");\nif (len(notice)) {\n    writeOutput(notice);\n    flashClear(); // reset Flash so it doesn't show again\n}\n\n3. Use before redirecting if you want to ensure no old flash values remain\nflashClear();\nredirectTo(action="index");\n
"},"hint":"The flashClear() function removes all keys and values from the Flash scope. This is useful when you want to reset or clear out any temporary messages or data that were carried over from a previous request. After calling flashClear(), the Flash will be empty for the remainder of the request and any future requests until new values are inserted.\n\n","returntype":"void","slug":"controller.flashClear","parameters":[],"availableIn":["controller"],"name":"flashClear","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get the number of items in Flash\ncount = flashCount();\n\n2. Check if there are any flash messages before displaying\nif (flashCount() > 0) {\n    writeOutput("You have " & flashCount() & " messages in Flash.");\n}\n\n3. Only display notice if Flash is not empty\nif (flashCount() > 0 && structKeyExists(flash(), "notice")) {\n    writeOutput(flash("notice"));\n}\n
"},"hint":"The flashCount() function returns the number of keys currently stored in the Flash scope. This is useful to check whether there are any flash messages or temporary data before attempting to read or display them. It helps in conditionally rendering notifications or determining if the Flash is empty.\n\n","returntype":"numeric","slug":"controller.flashCount","parameters":[],"availableIn":["controller"],"name":"flashCount","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Delete a single flash message\nflashDelete(key="errorMessage");\n\n2. Delete another key and check if it existed\nif (flashDelete(key="notice")) {\n    writeOutput("Notice deleted from Flash.");\n} else {\n    writeOutput("Notice key did not exist.");\n}\n\n3. Conditional usage before displaying flash\nif (structKeyExists(flash(), "warning")) {\n    warningMsg = flash("warning");\n    flashDelete(key="warning"); // remove after reading\n    writeOutput(warningMsg);\n}\n
"},"hint":"The flashDelete() function removes a specific key from the Flash scope. It is useful when you want to delete a particular temporary message or piece of data without clearing the entire Flash. The function returns true if the key existed and was deleted, or false if the key was not present.\n\n","returntype":"any","slug":"controller.flashDelete","parameters":[{"required":true,"hint":"The key to delete","name":"key","type":"string"}],"availableIn":["controller"],"name":"flashDelete","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Insert a simple flash message\nflashInsert(msg="It Worked!");\n\n2. Insert multiple types of data\nflashInsert(userId=123);\nflashInsert(errorMessage="Something went wrong");\n\n3. Typical usage: insert a message before redirecting\nflashInsert(notice="Profile updated successfully");\nredirectTo(action="show");\n\n4. Insert a structured value\nflashInsert(userStruct={id=42, name="Alice"});\n
"},"hint":"The flashInsert() function adds a new key-value pair to the Flash scope. This is useful for storing temporary messages or data that you want to persist across the next request, typically after a redirect. You can insert any type of value, such as strings, numbers, or structs, and later retrieve it using flash().\n\n","returntype":"void","slug":"controller.flashInsert","parameters":[],"availableIn":["controller"],"name":"flashInsert","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Check if the Flash is empty\nif (flashIsEmpty()) {\n    writeOutput("No messages to display.");\n} else {\n    writeOutput("There are messages in Flash.");\n}\n\n2. Use before reading a specific key\nif (!flashIsEmpty() && structKeyExists(flash(), "notice")) {\n    writeOutput(flash("notice"));\n}\n\n3. Typical flow: after clearing Flash\nflashClear();\nwriteOutput(flashIsEmpty()); // true\n
"},"hint":"The flashIsEmpty() function checks whether the Flash scope contains any keys. It returns true if the Flash is empty and false if it contains one or more keys. This is useful for conditionally displaying messages or deciding whether to process Flash data before reading or clearing it.\n\n","returntype":"boolean","slug":"controller.flashIsEmpty","parameters":[],"availableIn":["controller"],"name":"flashIsEmpty","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Keep the entire Flash for the next request\nflashKeep();\n\n2. Keep the "error" key in the Flash for the next request\nflashKeep("error");\n\n3. Keep both the "error" and "success" keys in the Flash for the next request\nflashKeep("error,success");
"},"hint":"The flashKeep() function allows you to preserve Flash data for one additional request. By default, Flash values are only available for the very next request; calling flashKeep() prevents them from being cleared after the current request. You can choose to keep the entire Flash or only specific keys. This is useful when you want messages or temporary data to persist through multiple redirects or page loads.\n\n","returntype":"void","slug":"controller.flashKeep","parameters":[{"default":"","required":false,"name":"key","type":"string"}],"availableIn":["controller"],"name":"flashKeep","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Check if the "error" key exists\nerrorExists = flashKeyExists("error");\n\n2. Conditional display based on key existence\nif (flashKeyExists("notice")) {\n    writeOutput(flash("notice"));\n}\n\n3. Example usage in a form flow\nif (flashKeyExists("validationErrors")) {\n    errors = flash("validationErrors");\n    // Process or display errors\n}\n
"},"hint":"The flashKeyExists() function checks whether a specific key is present in the Flash scope. It returns true if the key exists and false if it does not. This is useful for conditionally displaying or processing Flash messages or data before attempting to read them.\n\n","returntype":"boolean","slug":"controller.flashKeyExists","parameters":[{"required":true,"hint":"The key to check.","name":"key","type":"string"}],"availableIn":["controller"],"name":"flashKeyExists","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
// In the controller action\nflashInsert(success="Your post was successfully submitted.");\nflashInsert(alert="Don't forget to tweet about this post!");\nflashInsert(error="This is an error message.");\n\n<!--- In the layout or view --->\n#flashMessages()#\n\n<!--- Generates this (sorted alphabetically):--->\n<div class="flashMessages">\n\t<p class="alertMessage">\n\t\tDon't forget to tweet about this post!\n\t</p>\n\t<p class="errorMessage">\n\t\tThis is an error message.\n\t</p>\n\t<p class="successMessage">\n\t\tYour post was successfully submitted.\n\t</p>\n</div>\n\n\n<!---  Only show the "success" key in the view --->\n#flashMessages(key="success")#\n\n<!--- Generates this: --->\n<div class="flashMessage">\n\t<p class="successMessage">\n\t\tYour post was successfully submitted.\n\t</p>\n</div>\n\n\n<!--- Show only the "success" and "alert" keys in the view, in that order --->\n#flashMessages(keys="success,alert")#\n\n<!--- Generates this (sorted alphabetically):--->\n<div class="flashMessages">\n\t<p class="successMessage">\n\t\tYour post was successfully submitted.\n\t</p>\n\t<p class="alertMessage">\n\t\tDon't forget to tweet about this post!\n\t</p>\n</div>
"},"hint":"The flashMessages() function generates a formatted HTML output of messages stored in the Flash scope. It is typically used in views or layouts to display temporary notifications like success messages, alerts, or errors. You can choose to display all messages, a specific key, or multiple keys in a defined order. Additional options let you customize the container’s HTML class, include an empty container if no messages exist, and control whether the message content is URL-encoded.\n\n","returntype":"string","slug":"controller.flashMessages","parameters":[{"required":false,"hint":"The key (or list of keys) to show the value for. You can also use the `key` argument instead for better readability when accessing a single key.","name":"keys","type":"string"},{"default":"flash-messages","required":false,"hint":"HTML `class` to set on the `div` element that contains the messages.","name":"class","type":"string"},{"default":"false","required":false,"hint":"Includes the `div` container even if the Flash is empty.","name":"includeEmptyContainer","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"flashMessages","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: add a single float column\nt.float("price");\n\n2. Add multiple float columns at once\nt.float("length,width,height");\n\n3. Add a float column with a default value\nt.float(columnNames="discount", default="0.0");\n\n4. Add a float column that cannot be null\nt.float(columnNames="taxRate", allowNull=false);\n\n5. Add multiple float columns with defaults\nt.float(columnNames="latitude,longitude", default="0.0");\n\n6. Combine default value and null constraint\nt.float(columnNames="weight", default="1.0", allowNull=false);\n
"},"hint":"The float() function is used in a table definition during a migration to add one or more float-type columns to a database table. You can specify column names, default values, and whether the columns allow NULL. This helps define numeric columns with decimal values in your schema. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.float","parameters":[{"required":false,"name":"columnNames","type":"string"},{"default":"","required":false,"name":"default","type":"string"},{"default":"true","required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"float","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  post\n    // Example URL: /posts/my-post-title\n    // Controller:  Posts\n    // Action:      show\n    .get(name="post", pattern="posts/[slug]", to="posts##show")\n\n    // Route name:  posts\n    // Example URL: /posts\n    // Controller:  Posts\n    // Action:      index\n    .get(name="posts", controller="posts", action="index")\n\n    // Route name:  authors\n    // Example URL: /the-scribes\n    // Controller:  Authors\n    // Action:      index\n    .get(name="authors", pattern="the-scribes", to="authors##index")\n\n    // Route name:  commerceCart\n    // Example URL: /cart\n    // Controller:  commerce.Carts\n    // Action:      show\n    .get(name="cart", to="carts##show", package="commerce")\n\n    // Route name:  extranetEditProfile\n    // Example URL: /profile/edit\n    // Controller:  extranet.Profiles\n    // Action:      edit\n    .get(\n        name="editProfile",\n        pattern="profile/edit",\n        to="profiles##edit",\n        package="extranet"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="users", nested=true)\n        // Route name:  activatedUsers\n        // Example URL: /users/activated\n        // Controller:  Users\n        // Action:      activated\n        .get(name="activated", to="users##activated", on="collection")\n\n        // Route name:  preferencesUsers\n        // Example URL: /users/391/preferences\n        // Controller:  Preferences\n        // Action:      index\n        .get(name="preferences", to="preferences##index", on="member")\n    .end()\n.end();\n\n</cfscript>\n
"},"hint":"Create a route that matches a URL requiring an HTTP GET method. We recommend only using this matcher to expose actions that display data. See post, patch, delete, and put for matchers that are appropriate for actions that change data in your database.\n\n","returntype":"struct","slug":"mapper.get","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"get","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Get the current value of a global Wheels setting\ntablePrefix = get("tableNamePrefix");\n\n2. Get the default message for the `validatesConfirmationOf` function\nconfirmationMessageDefault = get(functionName="validatesConfirmationOf", name="message");\n\n3. Check the default value for the "null" argument in migrations\nallowNullDefault = get(functionName="float", name="null");\n\n4. Retrieve the current default number of rows per page in pagination\nperPageDefault = get("perPage");\n
"},"hint":"Returns the current value of a Wheels configuration setting or the default value for a specific function argument. It can be used to inspect global Wheels settings (like table name prefixes, pagination defaults, or other configuration values) or to check what the default argument would be for a particular Wheels function.\n\n","returntype":"any","slug":"controller.get","parameters":[{"required":true,"hint":"Variable name to get setting for.","name":"name","type":"string"},{"default":"","required":false,"hint":"Function name to get setting for.","name":"functionName","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"get","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"configuration","category":"Miscellaneous Functions","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Get all available migrations in the default folder\nmigrations = application.wheels.migrator.getAvailableMigrations();\n\n// Determine the latest migration version\nif (ArrayLen(migrations)) {\n    latestVersion = migrations[ArrayLen(migrations)].version;\n} else {\n    latestVersion = 0;\n}\n\n2. Get available migrations from a custom folder\ncustomMigrations = application.wheels.migrator.getAvailableMigrations(path="/custom/migrations");\n\n// Loop through migrations and display their versions\nfor (var m in migrations) {\n    writeOutput("Migration version: " & m.version & "<br>");\n}\n
"},"hint":"The getAvailableMigrations() function scans the migration folder (by default /app/migrator/migrations/) and returns an array of all migration files it finds. Each item in the array contains information about the migration, including its version. While this function can be called from within your application, it is primarily intended for use via the Wheels CLI or GUI tools. It is useful for programmatically determining which migrations are available and what the latest migration version is.\n\n","returntype":"array","slug":"migrator.getAvailableMigrations","parameters":[{"default":"[runtime expression]","required":false,"hint":"Path to Migration Files: defaults to /migrator/migrations/","name":"path","type":"string"}],"availableIn":["migrator"],"name":"getAvailableMigrations","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get the current database migration version\ncurrentVersion = application.wheels.migrator.getCurrentMigrationVersion();\nwriteOutput("Current DB version: " & currentVersion);\n\n// Compare with the latest available migration version\nmigrations = application.wheels.migrator.getAvailableMigrations();\nif (ArrayLen(migrations)) {\n    latestVersion = migrations[ArrayLen(migrations)].version;\n    if (currentVersion LT latestVersion) {\n        writeOutput("Database is behind the latest migration.");\n    } else {\n        writeOutput("Database is up-to-date.");\n    }\n}\n\n// Conditional logic based on migration version\nif (currentVersion EQ "2023091501") {\n    // perform tasks specific to this version\n}\n
"},"hint":"The getCurrentMigrationVersion() function returns the version number of the latest migration that has been applied to the database. This is useful for determining the current schema state programmatically, though it is primarily intended for use via the Wheels CLI or GUI interface. You can use this function within your application to perform conditional logic based on the database version or to verify that the database is up-to-date.\n\n","returntype":"string","slug":"migrator.getCurrentMigrationVersion","parameters":[],"availableIn":["migrator"],"name":"getCurrentMigrationVersion","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get all emails sent during the current request\nemails = getEmails();\n\n// Check if an email was sent to a specific recipient\nfor (var email in emails) {\n    if (email.to EQ "user@example.com") {\n        writeOutput("Email sent to user@example.com<br>");\n    }\n}\n
"},"hint":"Primarily used in testing scenarios to retrieve information about the emails that were sent during the current request. It returns an array containing details of all sent emails, which allows you to verify the content, recipients, and other properties of the emails in your automated tests. This is especially useful for unit or functional tests where you want to assert that specific emails are being triggered by certain actions without actually sending them.\n\n","returntype":"array","slug":"controller.getEmails","parameters":[],"availableIn":["controller"],"name":"getEmails","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get all files sent during the current request\nfiles = getFiles();\n\n// Check if a specific file was sent\nfor (var file in files) {\n    if (file.name EQ "report.pdf") {\n        writeOutput("File 'report.pdf' was sent.<br>");\n    }\n}\n
"},"hint":"The getFiles() function is primarily used in testing scenarios to retrieve information about files sent during the current request. It returns an array containing details of all files handled in the request, such as uploaded attachments or generated files. This allows you to inspect and verify file-related operations in automated tests without needing to access the file system directly.\n\n","returntype":"array","slug":"controller.getFiles","parameters":[],"availableIn":["controller"],"name":"getFiles","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get redirect information for the current request\nredirectInfo = getRedirect();\n\n// Check if a redirect occurred\nif (structKeyExists(redirectInfo, "url")) {\n    writeOutput("Redirected to: " & redirectInfo.url);\n    writeOutput("HTTP status: " & redirectInfo.status);\n} else {\n    writeOutput("No redirect occurred.");\n}\n
"},"hint":"Primarily used in testing scenarios to determine whether the current request has performed a redirect. It returns a structure containing information about the redirect, such as the target URL and the HTTP status code. This allows you to verify redirect behavior in automated tests without actually sending the user to another page.\n\n","returntype":"struct","slug":"controller.getRedirect","parameters":[],"availableIn":["controller"],"name":"getRedirect","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get all defined routes\nallRoutes = application.wheels.mapper.getRoutes();\n\n// Loop through routes and display their patterns\nfor (var r in allRoutes) {\n    writeOutput("Route name: " & r.name & "<br>");\n    writeOutput("Pattern: " & r.pattern & "<br>");\n    writeOutput("Controller: " & r.controller & "<br>");\n    writeOutput("Action: " & r.action & "<br><br>");\n}\n
"},"hint":"Returns all the routes that have been defined in the application via the mapper() function. It provides a programmatic way to inspect the routing table, including route names, URL patterns, controllers, actions, and other metadata. This is useful for debugging, generating dynamic links, or performing logic based on the routes that exist in your application.","returntype":"any","slug":"mapper.getRoutes","parameters":[],"availableIn":["mapper"],"name":"getRoutes","tags":{"categoryClass":"","sectionClass":"","category":"","section":""}},{"extended":{"hasExtended":true,"docs":"
1. Get the table name prefix for the current model\nprefix = model(\"user\").getTableNamePrefix();\nwriteOutput(\"Table prefix: \" & prefix);\n\n2. Get the table name prefix for this user when running a custom query.\n<cffunction name="getDisabledUsers" returntype="query">\n\t<cfquery datasource="#get('dataSourceName')#" name="local.disabledUsers">\n\tSELECT *\n\tFROM #this.getTableNamePrefix()#users\n\tWHERE disabled = 1\n\t</cfquery>\n\t<cfreturn local.disabledUsers>\n</cffunction>\n
"},"hint":"Returns the table name prefix that is set for the current model. This is useful when your database tables share a common prefix, and you need to construct queries dynamically or perform operations that require the full table name. By using this function, you ensure consistency and avoid hardcoding table prefixes in your queries.\n\n","returntype":"string","slug":"model.getTableNamePrefix","parameters":[],"availableIn":["model"],"name":"getTableNamePrefix","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get a member object and change the `email` property on it\nmember = model("member").findByKey(params.memberId);\nmember.email = params.newEmail;\n\n2. Check if the `email` property has changed\nif(member.hasChanged("email")){\n    // Do something...\n}\n\n3. The above can also be done using a dynamic function like this\nif(member.emailHasChanged()){\n    // Do something...\n}
"},"hint":"Returns true if the specified property (or any if none was passed in) has been changed but not yet saved to the database.\nWill also return true if the object is new and no record for it exists in the database.\n\n","returntype":"boolean","slug":"model.hasChanged","parameters":[{"default":"","required":false,"hint":"Name of property to check for change.","name":"property","type":"string"}],"availableIn":["model"],"name":"hasChanged","tags":{"categoryClass":"changefunctions","sectionClass":"modelobject","category":"Change Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Get a post object\npost = model("post").findByKey(params.postId);\n\n// Check if the object has any errors\nif (post.hasErrors()) {\n    writeOutput("There are errors. Redirecting to the edit form...");\n    redirectTo(action="edit", id=post.id);\n}\n\n2. Check if a specific property has errors\nif (post.hasErrors(property="title")) {\n    writeOutput("The title field contains errors.");\n}\n\n3. Check if a specific named error exists\nif (post.hasErrors(name="requiredTitle")) {\n    writeOutput("The post is missing a required title.");\n}\n
"},"hint":"Checks whether a model object has any validation or other errors. It returns true if the object contains errors, or if a specific property or named error is provided, it checks only that subset. This is useful for validating objects before saving them to the database or displaying error messages to the user.\n\n","returntype":"boolean","slug":"model.hasErrors","parameters":[{"default":"","required":false,"hint":"Name of the property to check if there are any errors set on.","name":"property","type":"string"},{"default":"","required":false,"hint":"Error name to check if there are any errors set with.","name":"name","type":"string"}],"availableIn":["model"],"name":"hasErrors","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Specify that instances of this model has many comments (the table for the associated model, not the current, should have the foreign key set on it).\nhasMany("comments");\n\n2. Specify that this model (let's call it `reader` in this case) has many subscriptions and setup a shortcut to the `publication` model (useful when dealing with many-to-many relationships).\nhasMany(name="subscriptions", shortcut="publications");\n\n3. Automatically delete all associated `comments` whenever this object is deleted.\nhasMany(name="comments", dependent="deleteAll");\n\n// When not following Wheels naming conventions for associations, it can get complex to define how a `shortcut` works.\n// In this example, we are naming our `shortcut` differently than the actual model's name.\n\n4. In the app/models/Customer.cfc `config()` method.\nhasMany(name="subscriptions", shortcut="magazines", through="publication,subscriptions");\n\n// In the app/models/Subscription.cfc `config()` method.\nbelongsTo("customer");\nbelongsTo("publication");\n\n// In the app/models/Publication `config()` method.\nhasMany("subscriptions");\n
"},"hint":"Sets up a one-to-many association between the current model and another model. This allows you to easily fetch, join, and manage related records in a relational way while following Wheels conventions.\n\n","returntype":"void","slug":"model.hasMany","parameters":[{"required":true,"hint":"Gives the association a name that you refer to when working with the association (in the `include` argument to `findAll`, to name one example).","name":"name","type":"string"},{"default":"","required":false,"hint":"Name of associated model (usually not needed if you follow Wheels conventions because the model name will be deduced from the `name` argument).","name":"modelName","type":"string"},{"default":"","required":false,"hint":"Foreign key property name (usually not needed if you follow Wheels conventions since the foreign key name will be deduced from the `name` argument).","name":"foreignKey","type":"string"},{"default":"","required":false,"hint":"Column name to join to if not the primary key (usually not needed if you follow Wheels conventions since the join key will be the table's primary key/keys).","name":"joinKey","type":"string"},{"default":"outer","required":false,"hint":"Use to set the join type when joining associated tables. Possible values are `inner` (for `INNER JOIN`) and `outer` (for `LEFT OUTER JOIN`).","name":"joinType","type":"string"},{"default":false,"required":false,"hint":"Defines how to handle dependent model objects when you delete an object from this model. `delete` / `deleteAll` deletes the record(s) (`deleteAll` bypasses object instantiation). `remove` / `removeAll` sets the forein key field(s) to `NULL` (`removeAll` bypasses object instantiation).","name":"dependent","type":"string"},{"default":"","required":false,"hint":"Set this argument to create an additional dynamic method that gets the object(s) from the other side of a many-to-many association.","name":"shortcut","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Set this argument if you need to override Wheels conventions when using the `shortcut` argument. Accepts a list of two association names representing the chain from the opposite side of the many-to-many relationship to this model.","name":"through","type":"string"}],"availableIn":["model"],"name":"hasMany","tags":{"categoryClass":"associationfunctions","sectionClass":"modelconfiguration","category":"Association Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage (Books -> Authors)\n\n// Loop through all authors and render checkboxes for associating them with the current book\n<cfloop query="authors">\n    #hasManyCheckBox(\n        objectName="book",\n        association="bookAuthors",\n        keys="#book.key()#,#authors.id#",\n        label=authors.fullName\n    )#\n</cfloop>\n\n2. Custom label placement\n\n// Place the label after the checkbox instead of before\n<cfloop query="categories">\n    #hasManyCheckBox(\n        objectName="post",\n        association="postCategories",\n        keys="#post.key()#,#categories.id#",\n        label=categories.name,\n        labelPlacement="after"\n    )#\n</cfloop>\n\n3. Wrapping checkboxes in extra HTML (prepend/append)\n\n// Use prepend and append to wrap checkboxes in a <div> with a custom class\n<cfloop query="tags">\n    #hasManyCheckBox(\n        objectName="article",\n        association="articleTags",\n        keys="#article.key()#,#tags.id#",\n        label=tags.name,\n        prepend='<div class="tag-option">',\n        append='</div>'\n    )#\n</cfloop>\n
"},"hint":"The hasManyCheckBox() helper generates the correct form elements for managing a hasMany or many-to-many association. It creates checkboxes for linking records together (e.g., a Book with many Authors).\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hasManyCheckBox","parameters":[{"required":true,"hint":"Name of the variable containing the parent object to represent with this form field.","name":"objectName","type":"string"},{"required":true,"hint":"Name of the association set in the parent object to represent with this form field.","name":"association","type":"string"},{"required":true,"hint":"Primary keys associated with this form field. Note that these keys should be listed in the order that they appear in the database table.","name":"keys","type":"string"},{"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using `aroundLeft` or `aroundRight`.","name":"labelPlacement","type":"string"},{"required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"required":false,"hint":"The `class` name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"hasManyCheckBox","tags":{"categoryClass":"formassociationfunctions","sectionClass":"viewhelpers","category":"Form Association Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage (Author -> Default Address)\n\n// Pick one address as the author’s default\n<cfloop query="addresses">\n    #hasManyRadioButton(\n        objectName="author",\n        association="authorsDefaultAddresses",\n        property="defaultAddressId",\n        keys="#author.key()#,#addresses.id#",\n        tagValue="#addresses.id#",\n        label=addresses.title\n    )#\n</cfloop>\n\n2. Pre-check default radio if property is blank\n\n// If no address is selected yet, pre-check the "Home" option\n<cfloop query="addresses">\n    #hasManyRadioButton(\n        objectName="author",\n        association="authorsDefaultAddresses",\n        property="defaultAddressId",\n        keys="#author.key()#,#addresses.id#",\n        tagValue="#addresses.id#",\n        label=addresses.title,\n        checkIfBlank=(addresses.title EQ "Home")\n    )#\n</cfloop>\n\n3. Style with extra HTML attributes\n\n// Add class and id for custom styling\n<cfloop query="paymentMethods">\n    #hasManyRadioButton(\n        objectName="user",\n        association="userPaymentMethods",\n        property="defaultPaymentMethodId",\n        keys="#user.key()#,#paymentMethods.id#",\n        tagValue="#paymentMethods.id#",\n        label=paymentMethods.name,\n        class="radio-option",\n        id="paymentMethod_#paymentMethods.id#"\n    )#\n</cfloop>\n
"},"hint":"This helper generates radio buttons for managing a hasMany or one-to-many association, where you want the user to pick one option (e.g., default address, primary contact method, preferred category).\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hasManyRadioButton","parameters":[{"required":true,"hint":"Name of the variable containing the parent object to represent with this form field.","name":"objectName","type":"string"},{"required":true,"hint":"Name of the association set in the parent object to represent with this form field.","name":"association","type":"string"},{"required":true,"hint":"Name of the property in the child object to represent with this form field.","name":"property","type":"string"},{"required":true,"hint":"Primary keys associated with this form field. Note that these keys should be listed in the order that they appear in the database table.","name":"keys","type":"string"},{"required":true,"hint":"The value of the radio button when selected.","name":"tagValue","type":"string"},{"default":false,"required":false,"hint":"Whether or not to check this form field as a default if there is a blank value set for the property.","name":"checkIfBlank","type":"boolean"},{"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"hasManyRadioButton","tags":{"categoryClass":"formassociationfunctions","sectionClass":"viewhelpers","category":"Form Association Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic one-to-one association\n\n// A User has one Profile. The profiles table has userId as the foreign key.\n// In app/models/User.cfc\nhasOne("profile");\n\n2. Strict inner join\n\n// Force that every Employee must have one PayrollRecord.\n// In app/models/Employee.cfc\nhasOne(name="payrollRecord", joinType="inner");\n\n// If there is no matching payrollRecord, the employee will not appear in queries using this association.\n\n3. Auto-delete dependent record\n\n// Delete the Profile when the User is deleted.\n// In app/models/User.cfc\nhasOne(name="profile", dependent="delete");\n\n4. Custom foreign key\n\n// If the foreign key doesn’t follow Wheels’ naming conventions.\n// For example, Driver has one License, but the foreign key column is driver_ref.\n// In app/models/Driver.cfc\nhasOne(name="license", foreignKey="driver_ref");\n\n5. Using joinKey for non-standard PK\n\n// If the Company table uses companyCode instead of id as the primary key, and the Address table has companyCode as the foreign key:\n// In app/models/Company.cfc\nhasOne(name="address", joinKey="companyCode");\n
"},"hint":"Defines a one-to-one relationship between two models. It means each instance of this model is linked to exactly one record in another model. By default, Wheels infers table and key names, but you can customize them with arguments like foreignKey, joinKey, and joinType.\n\n","returntype":"void","slug":"model.hasOne","parameters":[{"required":true,"hint":"Gives the association a name that you refer to when working with the association (in the `include` argument to `findAll`, to name one example).","name":"name","type":"string"},{"default":"","required":false,"hint":"Name of associated model (usually not needed if you follow Wheels conventions because the model name will be deduced from the `name` argument).","name":"modelName","type":"string"},{"default":"","required":false,"hint":"Foreign key property name (usually not needed if you follow Wheels conventions since the foreign key name will be deduced from the `name` argument).","name":"foreignKey","type":"string"},{"default":"","required":false,"hint":"Column name to join to if not the primary key (usually not needed if you follow Wheels conventions since the join key will be the table's primary key/keys).","name":"joinKey","type":"string"},{"default":"outer","required":false,"hint":"Use to set the join type when joining associated tables. Possible values are `inner` (for `INNER JOIN`) and `outer` (for `LEFT OUTER JOIN`).","name":"joinType","type":"string"},{"default":false,"required":false,"hint":"Defines how to handle dependent model objects when you delete an object from this model. `delete` / `deleteAll` deletes the record(s) (`deleteAll` bypasses object instantiation). `remove` / `removeAll` sets the forein key field(s) to `NULL` (`removeAll` bypasses object instantiation).","name":"dependent","type":"string"}],"availableIn":["model"],"name":"hasOne","tags":{"categoryClass":"associationfunctions","sectionClass":"modelconfiguration","category":"Association Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with existing property\nemployee = model("employee").new();\nemployee.firstName = "Alice";\n\nwriteOutput(employee.hasProperty("firstName")); // true\n\n2. Checking a property that does not exist\nemployee = model("employee").new();\n\nwriteOutput(employee.hasProperty("middleName")); // false\n\n3. Using the dynamic helper\nemployee = model("employee").new();\nemployee.email = "alice@example.com";\n\n// Equivalent to hasProperty("email")\nif (employee.hasEmail()) {\n    writeOutput("Email property exists!");\n}\n\n4. Before using a property safely\nuser = model("user").findByKey(1);\n\n// Avoid runtime errors by checking\nif (user.hasProperty("phoneNumber")) {\n    writeOutput(user.phoneNumber);\n} else {\n    writeOutput("No phone number property defined.");\n}\n
"},"hint":"Checks if a given property exists on a model object. It’s useful for safely validating whether a field is defined before accessing it, especially in dynamic code or when working with user input. This method also provides dynamic helpers (e.g., object.hasEmail()) for convenience.\n\n","returntype":"boolean","slug":"model.hasProperty","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"hasProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with object and property\n<!--- Hidden field for user.id --->\n#hiddenField(objectName="user", property="id")#\n\n// Generates something like:\n// <input id="user-id" name="user.id" type="hidden" value="123">\n\n2. Adding extra HTML attributes\n#hiddenField(\n    objectName="user",\n    property="sessionToken",\n    id="custom-token",\n    class="hidden-tracker"\n)#\n\n// <input id="custom-token" name="user.sessionToken" type="hidden" value="abc123" class="hidden-tracker">\n\n3. Nested association (hasOne or belongsTo)\n#hiddenField(\n    objectName="order",\n    property="id",\n    association="customer"\n)#\n\n// If an order has a customer, this binds the hidden field to order.customer.id.\n\n4. Nested hasMany with position\n#hiddenField(\n    objectName="order",\n    property="id",\n    association="items",\n    position="1"\n)#\n\n// Binds to the id of the second item in the order’s items collection.\n
"},"hint":"The hiddenField() function generates a hidden <input type=\"hidden\"> tag for a given model object and property. It’s commonly used to store identifiers or other values that need to persist across form submissions without being visible to the user.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hiddenField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"hiddenField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\n#hiddenFieldTag(name="userId", value=user.id)#\n\n// Generates:\n// <input id="userId" name="userId" type="hidden" value="123">\n\n2. With additional attributes\n#hiddenFieldTag(\n    name="sessionToken",\n    value="abc123",\n    id="token-field",\n    class="hidden-tracker"\n)#\n\n// <input id="token-field" name="sessionToken" type="hidden" value="abc123" class="hidden-tracker">\n\n3. Without specifying a value (empty by default)\n#hiddenFieldTag(name="csrfToken")#\n\n// <input id="csrfToken" name="csrfToken" type="hidden" value="">\n\n4. Disabling encoding\n#hiddenFieldTag(\n    name="redirectUrl",\n    value="https://example.com/?a=1&b=2",\n    encode=false\n)#\n\n// <input id="redirectUrl" name="redirectUrl" type="hidden" value="https://example.com/?a=1&b=2">\n
"},"hint":"Generates a hidden <input type=\"hidden\"> tag using a plain name/value pair. Unlike hiddenField(), this helper does not tie to a model object — it’s meant for raw form fields where you control the name and value manually.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.hiddenFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"hiddenFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage (default <span class="highlight">)\n#highlight(text="You searched for: Wheels", phrases="Wheels")#\n\n// Output:\n// You searched for: <span class="highlight">Wheels</span>\n\n2. Highlight multiple phrases\n#highlight(\n    text="ColdFusion and Wheels make development fun.",\n    phrases="ColdFusion,Wheels"\n)#\n\n// Output:\n// <span class="highlight">ColdFusion</span> and <span class="highlight">Wheels</span> make development fun.\n\n3. Use a custom delimiter for multiple phrases\n#highlight(\n    text="Apples | Oranges | Bananas",\n    phrases="Apples|Bananas",\n    delimiter="|"\n)#\n\n// Output:\n// <span class="highlight">Apples</span> | Oranges | <span class="highlight">Bananas</span>\n\n4. Use a different HTML tag\n#highlight(\n    text="Important: Read the documentation carefully.",\n    phrases="Important",\n    tag="strong"\n)#\n\n// Output:\n// <strong class="highlight">Important</strong>: Read the documentation carefully.\n
"},"hint":"Searches the given text for one or more phrases and wraps all matches in an HTML tag (default: <span>). This is useful for search results or emphasizing certain keywords dynamically.\n\n","returntype":"string","slug":"controller.highlight","parameters":[{"required":true,"hint":"Text to search in.","name":"text","type":"string"},{"required":false,"hint":"Phrase (or list of phrases) to highlight. This argument is also aliased as `phrases`.","name":"phrase","type":"string"},{"default":",","required":false,"hint":"Delimiter to use when passing in multiple phrases.","name":"delimiter","type":"string"},{"default":"span","required":false,"hint":"HTML tag to use to wrap the highlighted phrase(s).","name":"tag","type":"string"},{"default":"highlight","required":false,"hint":"Class to use in the tags wrapping highlighted phrase(s).","name":"class","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"highlight","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic 24-hour select\n#hourSelectTag(name="meetingHour")#\n\n// Output (simplified):\n// <select name="meetingHour">\n//   <option value="00">00</option>\n//   <option value="01">01</option>\n//   ...\n//   <option value="23">23</option>\n// </select>\n\n2. Pre-select an hour\n#hourSelectTag(name="meetingHour", selected="14")#\n\n// Output (simplified):\n// <option value="14" selected="selected">14</option>\n\n3. Include a blank option\n#hourSelectTag(name="meetingHour", includeBlank="- Select Hour -")#\n\n// Output (simplified):\n// <option value="">- Select Hour -</option>\n// <option value="00">00</option>\n// ...\n\n4. Use 12-hour format with AM/PM\n#hourSelectTag(name="meetingHour", twelveHour=true, selected="3")#\n\n// Output (simplified):\n// <select name="meetingHour">\n//   <option value="01">01</option>\n//   <option value="02">02</option>\n//   <option value="03" selected="selected">03</option>\n//   ...\n//   <option value="12">12</option>\n// </select>\n\n// <select name="meetingHourMeridian">\n//   <option value="AM">AM</option>\n//   <option value="PM">PM</option>\n// </select>\n
"},"hint":"Builds and returns a <select> form control for choosing an hour of the day. By default, hours are shown in 24-hour format (00–23), but you can switch to 12-hour format with an accompanying AM/PM dropdown.\n\n","returntype":"string","slug":"controller.hourSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"hourSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Humanize a string, will result in "Wheels Is A Framework"\n#humanize(text="wheelsIsAFramework")#\n\n2.Humanize a string, force wheels to replace "Cfml" with "CFML"\n#humanize(text="wheelsIsACfmlFramework", except="CFML")#
"},"hint":"Converts a camel-cased or underscored string into more readable, human-friendly text by inserting spaces and capitalizing words. You can also specify words that should be replaced or kept in a specific format.\n\n","returntype":"string","slug":"controller.humanize","parameters":[{"required":true,"hint":"Text to humanize.","name":"text","type":"string"},{"default":"","required":false,"hint":"A list of strings (space separated) to replace within the output.","name":"except","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"humanize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic camelCase string\n#hyphenize("myBlogPost")#\n\n// Output:\n// my-blog-post\n\n2. PascalCase string\n#hyphenize("UserProfileSettings")#\n\n// Output:\n// user-profile-settings\n\n3. Single word (no change)\n#hyphenize("Dashboard")#\n\n// Output:\n// dashboard\n\n4. Already hyphenated string (stays lowercase)\n#hyphenize("already-hyphenized")#\n\n// Output:\n// already-hyphenized\n
"},"hint":"Converts camelCase or PascalCase strings into lowercase hyphen-separated strings. Useful for generating URL-friendly slugs, CSS class names, or readable identifiers.\n\n","returntype":"string","slug":"controller.hyphenize","parameters":[{"required":true,"hint":"The string to hyphenize.","name":"string","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"hyphenize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Outputs an `img` tag for `public/images/logo.png`\n#imageTag("logo.png")#\n\n2. Outputs an `img` tag for `http://cfwheels.org/images/logo.png`\n#imageTag(source="http://cfwheels.org/images/logo.png", alt="ColdFusion on Wheels")#\n\n3. Outputs an `img` tag with the `class` attribute set\n#imageTag(source="logo.png", class="logo")#\n\n4. With explicit host and protocol\n#imageTag(source=\"logo.png\", onlyPath=false, host=\"cdn.myapp.com\", protocol=\"https\")#
"},"hint":"Returns an img tag.\nIf the image is stored in the local images folder, the tag will also set the width, height, and alt attributes for you.\nYou can pass any additional arguments (e.g. class, rel, id), and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.imageTag","parameters":[{"required":true,"hint":"The file name of the image if it's available in the local file system (i.e. ColdFusion will be able to access it). Provide the full URL if the image is on a remote server.","name":"source","type":"string"},{"default":true,"required":false,"name":"onlyPath","type":"boolean"},{"default":"","required":false,"name":"host","type":"string"},{"default":"","required":false,"name":"protocol","type":"string"},{"default":0,"required":false,"name":"port","type":"numeric"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"},{"default":true,"required":false,"name":"required","type":"boolean"}],"availableIn":["controller"],"name":"imageTag","tags":{"categoryClass":"assetfunctions","sectionClass":"viewhelpers","category":"Asset Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. In your view template, let's say `app/views/blog/post.cfm\ncontentFor(head='<meta name="robots" content="noindex,nofollow">');\ncontentFor(head='<meta name="author" content="wheelsdude@wheelsify.com">');\n\n// In `app/views/layout.cfm`\n<html>\n\t<head>\n\t    <title>My Site</title>\n\t    #includeContent("head")#\n\t</head>\n\t<body>\n\t\t<cfoutput>\n\t\t\t#includeContent()#\n\t\t</cfoutput>\n\t</body>\n</html>
"},"hint":"Outputs the content for a specific section in a layout. Works together with contentFor() to define and then inject content into layouts. Typically used for head, sidebar, footer, or other pluggable layout sections. If the requested section hasn’t been defined, it will either return nothing or the provided defaultValue.\n\n","returntype":"string","slug":"controller.includeContent","parameters":[{"default":"body","required":false,"hint":"Name of layout section to return content for.","name":"name","type":"string"},{"default":"","required":false,"hint":"What to display as a default if the section is not defined.","name":"defaultValue","type":"string"}],"availableIn":["controller"],"name":"includeContent","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Check to see if the customer is subscribed to the Swimsuit Edition. Note that the order of the `keys` argument should match the order of the `customerid` and `publicationid` columns in the `subscriptions` join table\nif(!includedInObject(objectName="customer", association="subscriptions", keys="#customer.key()#,#swimsuitEdition.id#")){\n    assignSalesman(customer);\n}
"},"hint":"Used as a shortcut to check if the specified IDs are a part of the main form object.\nThis method should only be used for hasMany associations.\n\n","returntype":"boolean","slug":"controller.includedInObject","parameters":[{"required":true,"hint":"Name of the variable containing the parent object to represent with this form field.","name":"objectName","type":"string"},{"required":true,"hint":"Name of the association set in the parent object to represent with this form field.","name":"association","type":"string"},{"required":true,"hint":"Primary keys associated with this form field. Note that these keys should be listed in the order that they appear in the database table.","name":"keys","type":"string"}],"availableIn":["controller"],"name":"includedInObject","tags":{"categoryClass":"formassociationfunctions","sectionClass":"viewhelpers","category":"Form Association Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Make sure that the `sidebar` value is provided for the parent layout\n<cfsavecontent variable="categoriesSidebar">\n\t<cfoutput>\n\t\t<ul>\n\t\t\t#includePartial(categories)#\n\t\t</ul>\n\t</cfoutput>\n</cfsavecontent>\ncontentFor(sidebar=categoriesSidebar);\n\n// Include parent layout at `app/views/layout.cfm`\n#includeLayout("/layout.cfm")#
"},"hint":"Includes the contents of another layout file. Typically used when a child layout wants to include a parent layout, or to nest layouts for consistent site structure.\n\n","returntype":"string","slug":"controller.includeLayout","parameters":[{"default":"layout","required":false,"hint":"Name of the layout file to include.","name":"name","type":"string"}],"availableIn":["controller"],"name":"includeLayout","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
\n1. If we're in the "sessions" controller, Wheels will include the file "app/views/sessions/_login.cfm".  \n#includePartial("login")# \n\n2. Wheels will include the file "app/views/shared/_button.cfm".  \n#includePartial(partial="/shared/button")# \n\n3. If we're in the "posts" controller and the "posts" variable includes a query result set, Wheels will loop through the record set and include the file "app/views/posts/_post.cfm" for each record.  \n<cfset posts = model("post").findAll()> \n#includePartial(posts)# \n\n4. We can also override the template file loaded for the example above.  \n#includePartial(partial="/shared/post", query=posts)# \n\n5. The same works when passing a model instance.  \n<cfset post = model("post").findByKey(params.key)> #includePartial(post)# \n#includePartial(partial="/shared/post", object=post)# \n\n6. The same works when passing an array of model objects.  \n<cfset posts = model("post").findAll(returnAs="objects")> \n#includePartial(posts)# \n#includePartial(partial="/shared/post", objects=posts)# \n</cfoutput>\n\n
"},"hint":"Includes the specified partial file in the view.\nSimilar to using cfinclude but with the ability to cache the result and use Wheels-specific file look-up.\nBy default, Wheels will look for the file in the current controller's view folder.\nTo include a file relative from the base views folder, you can start the path supplied to partial with a forward slash.\n\n","returntype":"string","slug":"controller.includePartial","parameters":[{"required":true,"hint":"The name of the partial file to be used. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Do not include the partial filename's underscore and file extension. If you want to have Wheels display the partial for a single model object, array of model objects, or a query, pass a variable containing that data into this argument.","name":"partial","type":"any"},{"default":"","required":false,"hint":"If passing a query result set for the partial argument, use this to specify the field to group the query by. A new query will be passed into the partial template for you to iterate over.","name":"group","type":"string"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"string"},{"default":"","required":false,"hint":"HTML or string to place between partials when called using a query.","name":"spacer","type":"string"},{"default":true,"required":false,"hint":"Name of controller function to load data from.","name":"dataFunction","type":"any"},{"default":true,"required":false,"name":"$prependWithUnderscore","type":"boolean"}],"availableIn":["controller"],"name":"includePartial","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single integer column age\nt.integer("age")\n\n2. Add multiple integer columns height and weight\nt.integer("height,weight")\n\n3. Add an integer column quantity with a default value of 0\nt.integer(columnNames="quantity", default=0)\n\n4. Add an integer column priority that cannot be null\nt.integer(columnNames="priority", allowNull=false)\n\n5. Add an integer column rating with a limit of 2 digits (smallint)\nt.integer(columnNames="rating", limit=2)\n\n6. Add multiple columns with different limits (comma-separated)\nt.integer(columnNames="smallValue,mediumValue,bigValue", limit=1)\n
"},"hint":"Adds one or more integer columns to a table definition during a migration. You can optionally specify a limit, default value, and whether the column allows NULL. Only available in migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.integer","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"numeric"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"integer","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. This is the method to be run inside a transaction.\npublic boolean function transferFunds(required any personFrom, required any personTo, required numeric amount) {\n\tif (arguments.personFrom.withdraw(arguments.amount) && arguments.personTo.deposit(arguments.amount)) {\n\t\treturn true;\n\t} else {\n\t\treturn false;\n\t}\n}\n\nlocal.david = model("Person").findOneByName("David");\nlocal.mary = model("Person").findOneByName("Mary");\ninvokeWithTransaction(method="transferFunds", personFrom=local.david, personTo=local.mary, amount=100);\n
"},"hint":"Runs a specified model method inside a single database transaction. This ensures that all database operations within the method are treated as a single atomic unit: either all succeed or all fail.\n\n","returntype":"any","slug":"model.invokeWithTransaction","parameters":[{"required":true,"hint":"Model method to run.","name":"method","type":"string"},{"default":"commit","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"read_committed","required":false,"hint":"Isolation level to be passed through to the cftransaction tag. See your CFML engine's documentation for more details about cftransaction's isolation attribute.","name":"isolation","type":"string"}],"availableIn":["model"],"name":"invokeWithTransaction","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Simple conditional logic\nif(isAjax()){\n    // Return JSON response for AJAX requests\n    cfcontent(type="application/json")\n    renderWith(data={ success = true, message = "This is an AJAX request" });\n} else {\n    // Render full HTML page for normal requests\n}\n\n2. Example in a Controller Action\ncomponent extends="Controller" {\n\n    function checkStatus() {\n        if (isAjax()) {\n            renderWith(data={ success = true, message = "This is an AJAX request" });\n        } else {\n            flashInsert(msg="Page loaded normally");\n            redirectTo("home");\n        }\n    }\n\n}
"},"hint":"Checks if the current request was made via JavaScript (AJAX) rather than a standard browser page load. This is useful when you want to return JSON or partial content instead of a full HTML page.\n\n","returntype":"boolean","slug":"controller.isAjax","parameters":[],"availableIn":["controller"],"name":"isAjax","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Use the passed in `id` when we're already in an instance\nfunction memberIsAdmin(){\n\tif(isClass()){\n\t\treturn this.findByKey(arguments.id).admin;\n\t} else {\n\t\treturn this.admin;\n\t}\n}\n
"},"hint":"Determines whether the method is being called at the class level (on the model itself) or on an instance of the model. This is useful when the same function can be invoked either on a model object or directly on the model class.\n\n","returntype":"string","slug":"model.isClass","parameters":[],"availableIn":["model"],"name":"isClass","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
component extends="Controller" {\n\n    public void function destroy() {\n        if (isDelete()) {\n            // Perform deletion logic\n            model("Post").deleteByKey(params.id);\n            flashInsert(success="Post deleted successfully.");\n            redirectTo(action="index");\n        } else {\n            // Handle non-DELETE request\n            flashInsert(error="Invalid request method.");\n            redirectTo(action="index");\n        }\n    }\n\n}
"},"hint":"Checks if the current HTTP request method is DELETE. This is useful for RESTful controllers where different logic is executed based on the request type.\n\n","returntype":"boolean","slug":"controller.isDelete","parameters":[],"availableIn":["controller"],"name":"isDelete","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
component extends="Controller" {\n\n    public void function show() {\n        if (isGet()) {\n            // Display a form or data\n            post = model("Post").findByKey(params.id);\n            renderWith(action="show", data=post);\n        } else {\n            // Handle non-GET request (e.g., POST, DELETE)\n            flashInsert(error="Invalid request method.");\n            redirectTo(action="index");\n        }\n    }\n\n}
"},"hint":"Checks if the current HTTP request method is GET. Useful for controlling logic depending on whether a page is being displayed or data is being requested via GET.\n\n","returntype":"boolean","slug":"controller.isGet","parameters":[],"availableIn":["controller"],"name":"isGet","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
component extends="Controller" {\n\n    public void function checkFile() {\n        if (isHead()) {\n            // Respond with headers only, no content\n        } else {\n            // Handle normal GET or other requests\n            fileData = model("File").findByKey(params.id);\n            renderWith(action="show", data=fileData);\n        }\n    }\n\n}
"},"hint":"Checks if the current HTTP request method is HEAD. HEAD requests are similar to GET requests but do not return a message body, only the headers. This is often used for checking metadata like content length or existence without transferring the actual content.\n\n","returntype":"boolean","slug":"controller.isHead","parameters":[],"availableIn":["controller"],"name":"isHead","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Use the passed in `id` when we're not already in an instance\nfunction memberIsAdmin(){\n\tif(isInstance()){\n\t\treturn this.admin;\n\t} else {\n\t\treturn this.findByKey(arguments.id).admin;\n\t}\n}\n
"},"hint":"Checks whether the current context is an instance of a model object rather than a class-level context. This is useful when a method could be called either on a class or an instance, and you want to behave differently depending on which it is.\n\n","returntype":"boolean","slug":"model.isInstance","parameters":[],"availableIn":["model"],"name":"isInstance","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Create a new object and then check if it is new (yes, this example is ridiculous. It makes more sense in the context of callbacks for example)\nemployee = model("employee").new()>\n<cfif employee.isNew()>\n    // Do something...\n</cfif>
"},"hint":"Returns true if this object hasn't been saved yet (in other words, no matching record exists in the database yet).\nReturns false if a record exists.\n\n","returntype":"boolean","slug":"model.isNew","parameters":[],"availableIn":["model"],"name":"isNew","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\nif (isOptions()) {\n   // Handle CORS preflight or respond to OPTIONS request\n   writeOutput("This is an OPTIONS request.");\n} else {\n   writeOutput("This is a different type of request.");\n}\n</cfscript>
"},"hint":"Checks whether the current HTTP request was made using the OPTIONS method. Useful in REST APIs or CORS preflight requests.\n\n","returntype":"boolean","slug":"controller.isOptions","parameters":[],"availableIn":["controller"],"name":"isOptions","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\nif (isPatch()) {\n    writeOutput("This is a PATCH request.");\n} else {\n    writeOutput("This is a different type of request.");\n}\n</cfscript>
"},"hint":"Checks whether the current HTTP request was made using the PATCH method. Useful when building RESTful APIs where PATCH is used to partially update resources.\n\n","returntype":"boolean","slug":"controller.isPatch","parameters":[],"availableIn":["controller"],"name":"isPatch","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Check an older object\nemployee = model("employee").findByKey(123);\nif (employee.isPersisted()) {\n    writeOutput("This employee exists in the database.");\n} else {\n    writeOutput("This employee has not been saved yet.");\n}\n\n2. Creating a new object\nnewEmployee = model("employee").new();\nif (!newEmployee.isPersisted()) {\n    writeOutput("This is a new object, not yet persisted.");\n}
"},"hint":"Returns true if this object has been persisted to the database or was loaded from the database via a finder.\nReturns false if the record has not been persisted to the database.\n\n","returntype":"boolean","slug":"model.isPersisted","parameters":[],"availableIn":["model"],"name":"isPersisted","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
if (isPost()) {\n    writeOutput("This request was submitted via POST.");\n} else {\n    writeOutput("This request is not a POST request.");\n}
"},"hint":"Returns whether the request came from a form POST submission or not.\n\n","returntype":"boolean","slug":"controller.isPost","parameters":[],"availableIn":["controller"],"name":"isPost","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
if (isPut()) {\n    writeOutput("This request was submitted via PUT.");\n} else {\n    writeOutput("This request is not a PUT request.");\n}
"},"hint":"Checks whether the current HTTP request is a PUT request. PUT requests are typically used to update existing resources in RESTful APIs.\n\n","returntype":"boolean","slug":"controller.isPut","parameters":[],"availableIn":["controller"],"name":"isPut","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Redirect non-secure connections to the secure version\nif (!isSecure())\n{\n\tredirectTo(protocol="https");\n}
"},"hint":"Checks whether the current request is made over a secure connection (HTTPS). Returns true if the connection is secure, otherwise false.\n\n","returntype":"boolean","slug":"controller.isSecure","parameters":[],"availableIn":["controller"],"name":"isSecure","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<!--- View Code --->\n<head>\n    <!--- Includes `public/javascripts/main.js` --->\n    #javaScriptIncludeTag("main")#\n\n    <!--- Includes `publicjavascripts/blog.js` and `public/javascripts/accordion.js` --->\n    #javaScriptIncludeTag("blog,accordion")#\n    \n    <!--- Includes external JavaScript file --->\n    #javaScriptIncludeTag("https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js")#\n</head>\n\n<body>\n    <!--- Will still appear in the `head` --->\n    #javaScriptIncludeTag(source="tabs", head=true, type=\"text/javascript\")#\n</body>\n\n
"},"hint":"Generates <script> tags for including JavaScript files. Can handle local files in the javascripts folder or external URLs. Supports multiple files and optional placement in the HTML <head>.\n\n","returntype":"string","slug":"controller.javaScriptIncludeTag","parameters":[{"default":"","required":false,"hint":"The name of one or many JavaScript files in the `javascripts` folder, minus the `.js` extension. Pass a full URL to access an external JavaScript file. Can also be called with the `source` argument.","name":"sources","type":"string"},{"default":"text/javascript","required":false,"hint":"The `type` attribute for the `script` tag.","name":"type","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to place the output in the `head` area of the HTML page instead of the default behavior (which is to place the output where the function is called from).","name":"head","type":"boolean"},{"default":",","required":false,"hint":"The delimiter to use for the list of JavaScript files.","name":"delim","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"javaScriptIncludeTag","tags":{"categoryClass":"assetfunctions","sectionClass":"viewhelpers","category":"Asset Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Single Primary Key\n\nemployee = model("employee").findByKey(42);\n\n<cfoutput>\nEmployee ID: #employee.key()# <!--- Equivalent to employee.id --->\n</cfoutput>\n\n2. Dynamic Key Retrieval\n\nanyEmployee = model("employee").findByKey(params.key);\n\nprimaryKey = anyEmployee.key();\nwriteOutput("Primary key value is: " & primaryKey);\n\n3. Composite Primary Key\n\nsubscription = model("subscription").findByKey(customerId=3, publicationId=7);\n\n<cfoutput>\nComposite Keys: #subscription.key()# <!--- Outputs: "3,7" --->\n</cfoutput>\n\n4. Use in Links or Forms\n<cfset employee = model("employee").findByKey(42)>\n\n<!--- Generate a link with dynamic primary key --->\n<a href="#linkTo(action='edit', id=employee.key())#">Edit Employee</a>\n\n<!--- Hidden field for a form --->\n#hiddenField(objectName="employee", property="id")#\n\n5. Passing Keys in Nested Relationships\n<!--- Suppose a `bookAuthors` association exists --->\nbook = model("book").findByKey(15);\n\n<cfloop array="#book.bookAuthors#" index="author">\n    <cfoutput>\n        Author Key: #author.key()# <br>\n    </cfoutput>\n</cfloop>
"},"hint":"Returns the value of the primary key for the object.\nIf you have a single primary key named id, then someObject.key() is functionally equivalent to someObject.id.\nThis method is more useful when you do dynamic programming and don't know the name of the primary key or when you use composite keys (in which case it's convenient to use this method to get a list of both key values returned).\n\n","returntype":"string","slug":"model.key","parameters":[{"default":false,"required":false,"name":"$persisted","type":"boolean"},{"default":false,"required":false,"name":"$returnTickCountWhenNew","type":"boolean"}],"availableIn":["model"],"name":"key","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
#linkTo(text="Log Out", controller="account", action="logout")#\n<!--- Ouputs: <a href="/account/logout">Log Out</a> --->\n\n<!--- If you're already in the `account` controller, Wheels will assume that's where you want the link to point --->\n#linkTo(text="Log Out", action="logout")#\n<!--- Ouputs: <a href="/account/logout">Log Out</a> --->\n\n#linkTo(text="View Post", controller="blog", action="post", key=99)#\n<!--- Ouputs: <a href="/blog/post/99">View Post</a> --->\n\n#linkTo(text="View Settings", action="settings", params="show=all&amp;sort=asc")#\n<!--- Ouputs: <a href="/account/settings?show=all&amp;amp;sort=asc">View Settings</a> --->\n\n<!--- Given that a `userProfile` route has been configured in `config/routes.cfm` --->\n#linkTo(text="Joe's Profile", route="userProfile", userName="joe")#\n<!--- Ouputs: <a href="/user/joe">Joe's Profile</a> --->\n\n<!--- Link to an external website --->\n#linkTo(text="ColdFusion Framework", href="http://cfwheels.org/")#\n<!--- Ouputs: <a href="http://cfwheels.org/">ColdFusion Framework</a> --->\n\n<!--- Give the link `class` and `id` attributes --->\n#linkTo(text="Delete Post", action="delete", key=99, class="delete", id="delete-99")#\n<!--- Ouputs: <a class="delete" href="/blog/delete/99" id="delete-99">Delete Post</a> --->\n\n
"},"hint":"Creates a link to another page in your application.\nPass in the name of a route to use your configured routes or a controller/action/key combination.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.linkTo","parameters":[{"required":false,"hint":"The text content of the link.","name":"text","type":"string"},{"default":"","required":false,"hint":"Name of a route that you have configured in config/routes.cfm.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: wheels=cool&x=y). Please note that Wheels uses the & and = characters to split the parameters and encode them properly for you. However, if you need to pass in & or = as part of the value, then you need to encode them (and only them), example: a=cats%26dogs%3Dtrouble!&b=1.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If true, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"required":false,"hint":"Pass a link to an external site here if you want to bypass the Wheels routing system altogether and link to an external URL.","name":"href","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"linkTo","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic mailto link\n#mailTo(emailAddress="webmaster@yourdomain.com")#\n<!--- Outputs: <a href="mailto:webmaster@yourdomain.com">webmaster@yourdomain.com</a> --->\n\n2. Mailto link with custom name\n#mailTo(emailAddress="webmaster@yourdomain.com", name="Contact our Webmaster")#\n<!--- Outputs: <a href="mailto:webmaster@yourdomain.com">Contact our Webmaster</a> --->\n\n3. Mailto link with special characters (encoding)\n#mailTo(emailAddress="support+help@yourdomain.com", name="Support Team")#\n<!--- Outputs: <a href="mailto:support+help@yourdomain.com">Support Team</a> --->
"},"hint":"Creates a mailto link tag to the specified email address, which is also used as the name of the link unless name is specified.\n\n","returntype":"string","slug":"controller.mailTo","parameters":[{"required":true,"hint":"The email address to link to.","name":"emailAddress","type":"string"},{"default":"","required":false,"hint":"A string to use as the link text (\"Joe\" or \"Support Department\", for example).","name":"name","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"mailTo","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Usage\n<cfscript>\nmapper()\n    .resources("posts")  // generates standard RESTful routes for posts\n    .get(name="about", pattern="about-us", to="pages##about") // custom GET route\n    .namespace("admin") // group routes under admin namespace\n        .resources("users") // RESTful routes for admin users\n    .end();\n</cfscript>\n\n2. Disable format mapping\nmapper(mapFormat=false)\n    .resources("reports");\n\n// This will prevent automatic generation of .json or .xml endpoints for the resource.
"},"hint":"Returns the mapper object used to configure your application's routes. Usually you will use this method in app/config/routes.cfm to start chaining route mapping methods like resources, namespace, etc.\n\n","returntype":"struct","slug":"controller.mapper","parameters":[{"default":true,"required":false,"hint":"Whether to turn on RESTful routing or not. Not recommended to set. Will probably be removed in a future version of wheels, as RESTful routes are the default.","name":"restful","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If not RESTful, then specify allowed routes. Not recommended to set. Will probably be removed in a future version of wheels, as RESTful routes are the default.","name":"methods","type":"boolean"},{"default":true,"required":false,"hint":"This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc. Set to false to disable automatic .[format] generation for resource based routes","name":"mapFormat","type":"boolean"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"mapper","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Maximum value for all records\nhighestSalary = model("employee").maximum("salary");\n// one-liner: highestSalary = model("employee").maximum("salary");\n\n2. Maximum value with a WHERE condition\nhighestSalary = model("employee").maximum(\n    property="salary", \n    where="departmentId=#params.departmentId#"\n);\n// one-liner: highestSalary = model("employee").maximum(property="salary", where="departmentId=#params.departmentId#");\n\n3. Maximum value with a default if no records found\nhighestSalary = model("employee").maximum(\n    property="salary", \n    where="salary > #params.minSalary#", \n    ifNull=0\n);\n// one-liner: highestSalary = model("employee").maximum(property="salary", where="salary > #params.minSalary#", ifNull=0);\n\n4. Maximum value including associations (nested join)\nhighestAlbumSales = model("album").maximum(\n    property="sales",\n    include="artist(genre)"\n);\n// one-liner: highestAlbumSales = model("album").maximum(property="sales", include="artist(genre)");\n\n5. Maximum value grouped by a column\nmaxSalaryByDept = model("employee").maximum(\n    property="salary",\n    group="departmentId"\n);\n// one-liner: maxSalaryByDept = model("employee").maximum(property="salary", group="departmentId");
"},"hint":"Calculates the maximum value for a given property.\nUses the SQL function MAX.\nIf no records can be found to perform the calculation on you can use the ifNull argument to decide what should be returned.\n\n","returntype":"any","slug":"model.maximum","parameters":[{"required":true,"hint":"Name of the property to get the highest value for (must be a property of a numeric data type).","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":true,"required":false,"name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"maximum","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Create a route like `photos/1/preview`\n    .resources(name="photos", nested=true)\n        .member()\n            .get("preview")\n        .end()\n    .end()\n.end();\n\n</cfscript>\n
"},"hint":"Scope routes within a nested resource which require use of the primary key as part of the URL pattern;\nA member route will require an ID, because it acts on a member.\nphotos/1/preview is an example of a member route, because it acts on (and displays) a single object.\n\n","returntype":"struct","slug":"mapper.member","parameters":[],"availableIn":["mapper"],"name":"member","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Migrate to a specific version\n// Returns a message with the result\nresult=application.wheels.migrator.migrateTo(version);\n
"},"hint":"Migrates the database schema to a specified version. This function is primarily intended for programmatic database migrations, but the recommended usage is via the CLI or Wheels GUI interface.\n\n","returntype":"string","slug":"migrator.migrateTo","parameters":[{"default":"","required":false,"hint":"The Database schema version to migrate to","name":"version","type":"string"}],"availableIn":["migrator"],"name":"migrateTo","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
// Migrate database to the latest version\nresult = application.wheels.migrator.migrateToLatest();\n\n// Output the result message\nwriteOutput(result);
"},"hint":"Migrates the database schema to the latest available migration version. This is a shortcut for migrateTo(version) without needing to specify a version explicitly.\n\n","returntype":"string","slug":"migrator.migrateToLatest","parameters":[],"availableIn":["migrator"],"name":"migrateToLatest","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Known Extension\n// Get the MIME type for a known extension\nmimeType = mimeTypes("jpg");\nwriteOutput(mimeType); // Outputs: "image/jpeg"\n\n2. Unknown Extension With Fallback\n// Use a fallback for unknown file types\nmimeType = mimeTypes("abc", fallback="text/plain");\nwriteOutput(mimeType); // Outputs: "text/plain"\n\n3. Dynamic Extension From User Input\nparams.type = "pdf";\nmimeType = mimeTypes(extension=params.type);\nwriteOutput(mimeType); // Outputs: "application/pdf"\n\n4. Serving a File Download\nfileName = "report.xlsx";\nfileExt = listLast(fileName, ".");\ncfheader(name="Content-Disposition", value="attachment; filename=#fileName#");\ncfcontent(type=mimeTypes(fileExt), file="#expandPath('./public/files/' & fileName)#");
"},"hint":"Returns the associated MIME type for a given file extension. Useful when serving files dynamically or setting response headers.\n\n","returntype":"string","slug":"controller.mimeTypes","parameters":[{"required":true,"hint":"The extension to get the MIME type for.","name":"extension","type":"string"},{"default":"application/octet-stream","required":false,"hint":"The fallback MIME type to return.","name":"fallback","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"mimeTypes","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Minimum Value\n// Get the lowest salary among all employees\nlowestSalary = model("employee").minimum("salary");\nwriteOutput("Lowest Salary: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum("salary"));\n\n2. Minimum Value with Condition\n// Get the lowest salary for employees in a specific department\ndeptId = 5;\nlowestSalary = model("employee").minimum(\n    property="salary",\n    where="departmentId=#deptId#"\n);\nwriteOutput("Lowest Salary in Department #deptId#: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum(property="salary", where="departmentId=5"));\n\n3. Minimum Value with Range and Fallback\n// Get the lowest salary within a range and fallback to 0 if no records\nlowestSalary = model("employee").minimum(\n    property="salary",\n    where="salary BETWEEN #params.min# AND #params.max#",\n    ifNull=0\n);\nwriteOutput("Lowest Salary in range: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum(property="salary", where="salary BETWEEN #params.min# AND #params.max#", ifNull=0));\n\n4. Including Associations\n// Get the lowest product price including related categories\nlowestPrice = model("product").minimum(\n    property="price",\n    include="category"\n);\nwriteOutput("Lowest Product Price: " & lowestPrice);\n// inline: writeOutput(model("product").minimum(property="price", include="category"));\n\n5. Include Soft-Deleted Records\n// Include soft-deleted employees in the calculation\nlowestSalary = model("employee").minimum(\n    property="salary",\n    includeSoftDeletes=true\n);\nwriteOutput("Lowest Salary including soft-deleted employees: " & lowestSalary);\n// inline: writeOutput(model("employee").minimum(property="salary", includeSoftDeletes=true));
"},"hint":"Calculates the minimum value for a specified property in a model using SQL's MIN() function. This can be used to find the lowest value of a numeric property across all records or with conditions. You can also include associations, handle soft-deleted records, provide fallback values, and group results. If no records can be found to perform the calculation on you can use the ifNull argument to decide what should be returned.\n\n","returntype":"any","slug":"model.minimum","parameters":[{"required":true,"hint":"Name of the property to get the lowest value for (must be a property of a numeric data type).","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"minimum","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Minute Select\nminuteSelectTag(name="minuteOfMeeting", selected=params.minuteOfMeeting)\n\n2. 15-Minute Intervals\nminuteSelectTag(name="minuteOfMeeting", selected=params.minuteOfMeeting, minuteStep=15)\n\n3. Include Blank Option\nminuteSelectTag(name="minuteOfMeeting", includeBlank="- Select Minute -")\n\n4. Using Label\nminuteSelectTag(name="minuteOfMeeting", label="Select Minute")\n\n5. Custom Label Placement\nminuteSelectTag(name="minuteOfMeeting", label="Minute", labelPlacement="after")\n\n
"},"hint":"Builds and returns a <select> dropdown for the minutes of an hour (0–59). You can customize the selected value, increment steps (e.g., 5, 10, 15 minutes), label placement, and include a blank option. Useful for forms where users pick a time.\n\n","returntype":"string","slug":"controller.minuteSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"minuteSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
// The `model("author")` part of the code below gets a reference to the model from the application scope, and then the `findByKey` class level method is called on it\nauthorObject = model("author").findByKey(1);
"},"hint":"Returns a reference to a specific model defined in your application, allowing you to call class-level methods on it. This is useful when you want to access database records or invoke model methods without instantiating a new object first.\n\n","returntype":"any","slug":"controller.model","parameters":[{"required":true,"hint":"Name of the model to get a reference to.","name":"name","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"model","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\nmonthSelectTag(name="monthOfBirthday", selected=params.monthOfBirthday)\n\n2. Display months as numbers\nmonthSelectTag(name="monthOfHire", selected=3, monthDisplay="numbers")\n\n3. Display months as abbreviations\nmonthSelectTag(name="monthOfEvent", selected="Jun", monthDisplay="abbreviations")\n\n4. Include a blank option\nmonthSelectTag(name="monthOfAppointment", includeBlank="- Select Month -")\n\n5. Custom label and wrapping\nmonthSelectTag(name="monthOfSubscription", label="Subscription Month:", labelPlacement="before")\n
"},"hint":"Generates a <select> dropdown for selecting a month. You can customize its options, labels, and display format. Unlike dateSelect, this function focuses only on the month portion.\n\n","returntype":"string","slug":"controller.monthSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The month that should be selected initially.","name":"selected","type":"string"},{"default":"names","required":false,"hint":"Pass in names, numbers, or abbreviations to control display.","name":"monthDisplay","type":"string"},{"default":"January,February,March,April,May,June,July,August,September,October,November,December","required":false,"hint":"[see:dateSelect].","name":"monthNames","type":"string"},{"default":"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec","required":false,"hint":"[see:dateSelect].","name":"monthAbbreviations","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"monthSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    .namespace("api")\n        .namespace("v2")\n            // Route name:  apiV2Products\n            // Example URL: /api/v2/products/1234\n            // Controller:  api.v2.Products\n            .resources("products")\n        .end()\n\n        .namespace("v1")\n            // Route name:  apiV1Users\n            // Example URL: /api/v1/users\n            // Controller:  api.v1.Users\n            .get(name="users", to="users##index")\n        .end()\n    .end()\n\n    .namespace(name="foo", package="foos", path="foose")\n        // Route name:  fooBars\n        // Example URL: /foose/bars\n        // Controller:  foos.Bars\n        .post(name="bars", to="bars##create")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"The namespace() function in Wheels is used to group controllers and routes under a specific namespace (subfolder/package). It also prepends the namespace to route names and can modify the URL path. This is useful for organizing APIs, versioning, or modular applications. Namespaces can be nested for hierarchical routing, e.g., /api/v1/... and /api/v2/....\n\n","returntype":"struct","slug":"mapper.namespace","parameters":[{"required":true,"hint":"Name to prepend to child route names.","name":"name","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Subfolder (package) to reference for controllers. This defaults to the value provided for `name`.","name":"package","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Subfolder path to add to the URL.","name":"path","type":"string"}],"availableIn":["mapper"],"name":"namespace","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic nested association with auto-save\n// app/models/User.cfc\nfunction config(){\n    hasMany("groupEntitlements");\n\n    // Allow nested save of `groupEntitlements` when user is saved\n    nestedProperties(association="groupEntitlements");\n}\n\n// Controller code\nuser = model("User").findByKey(1);\nuser.groupEntitlements = [\n    {groupId=1, role="admin"},\n    {groupId=2, role="editor"}\n];\nuser.save(); \n// Both the user and nested groupEntitlements are saved automatically\n\n2. Allow deletion of nested objects\nfunction config(){\n    hasMany("groupEntitlements");\n\n    // Enable deletion via `_delete` flag\n    nestedProperties(association="groupEntitlements", allowDelete=true);\n}\n\n// Example params\nparams.user.groupEntitlements = [\n    {id=10, _delete=true},\n    {groupId=3, role="viewer"}\n];\n\nuser = model("User").findByKey(params.user.id);\nuser.setProperties(params.user);\nuser.save();\n// The first nested object (id=10) is deleted, the second is saved\n
"},"hint":"Allows nested objects, arrays, or structs associated with a model to be automatically set from incoming params or other generated data. This is particularly useful when you have hasMany or belongsTo associations and want to manage them directly when saving the parent object.\n\n","returntype":"void","slug":"model.nestedProperties","parameters":[{"default":"","required":false,"hint":"The association (or list of associations) you want to allow to be set through the params. This argument is also aliased as `associations`.","name":"association","type":"string"},{"default":true,"required":false,"hint":"Whether to save the association(s) when the parent object is saved.","name":"autoSave","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to tell Wheels to look for the property `_delete` in your model. If present and set to a value that evaluates to true, the model will be deleted when saving the parent.","name":"allowDelete","type":"boolean"},{"default":"","required":false,"hint":"Set this to a property on the object that you would like to sort by. The property should be numeric, should start with 1, and should be consecutive. Only valid with `hasMany` associations.","name":"sortProperty","type":"string"},{"default":"","required":false,"hint":"A list of properties that should not be blank. If any of the properties are blank, any CRUD operations will be rejected.","name":"rejectIfBlank","type":"string"}],"availableIn":["model"],"name":"nestedProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Create a new author in memory (not saved to the database)\nnewAuthor = model("author").new();\n\n2. Create a new author based on properties in a struct\nnewAuthor = model("author").new(params.authorStruct);\n\n3. Create a new author by passing in named arguments\nnewAuthor = model("author").new(firstName="John", lastName="Doe");\n\n4. If you have a `hasOne` or `hasMany` association setup from `customer` to `order`, you can do a scoped call. (The `newOrder` method below will call `model("order").new(customerId=aCustomer.id)` internally.)\naCustomer = model("customer").findByKey(params.customerId);\nanOrder = aCustomer.newOrder(shipping=params.shipping);\n\n5. Allow explicit timestamps\n// You can manually set createdAt and updatedAt fields\nnewAuthor = model(\"author\").new(\n    firstName=\"Bob\",\n    lastName=\"Builder\",\n    allowExplicitTimestamps=true,\n    createdAt=createDate(2025,9,24),\n    updatedAt=createDate(2025,9,24)\n);
"},"hint":"Creates a new object based on supplied properties and returns it.\nThe object is not saved to the database, it only exists in memory.\nProperty names and values can be passed in either using named arguments or as a struct to the properties argument.\n\n","returntype":"any","slug":"model.new","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to allow explicit assignment of `createdAt` or `updatedAt` properties","name":"allowExplicitTimestamps","type":"boolean"}],"availableIn":["model"],"name":"new","tags":{"categoryClass":"createfunctions","sectionClass":"modelclass","category":"Create Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Obfuscate a numeric primary key\n// Primary key value\nid = 99;\n\n// Obfuscate it before sending in the URL\nobfuscatedId = obfuscateParam(id);\nwriteOutput(obfuscatedId); \n\n2. Obfuscate a string value\n// Obfuscate an email address\nemail = "user@example.com";\nobfuscatedEmail = obfuscateParam(email);\nwriteOutput(obfuscatedEmail); \n\n3. Use obfuscated value in a link\n// Pass obfuscated ID in a linkTo helper\nuserId = 42;\n#linkTo(text="View Profile", controller="user", action="profile", key=obfuscateParam(userId))#\n
"},"hint":"Obfuscates a value, typically used to hide sensitive information like primary key IDs when passing them in URLs. This helps prevent users from easily guessing sequential IDs or sensitive values.\n\n","returntype":"string","slug":"controller.obfuscateParam","parameters":[{"required":true,"hint":"The value to obfuscate.","name":"param","type":"any"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"obfuscateParam","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Restrict an action to HTML only\nfunction show() {\n    // This action will only respond with HTML\n    onlyProvides("html");\n}\n\n2. Restrict an action to JSON and XML\nfunction data() {\n    // Only allow JSON or XML responses\n    onlyProvides("json,xml");\n}\n\n3. Override global provides setting\ncomponent extends="Controller" {\n\n    function config() {\n        // Globally allow HTML and JSON\n        provides("html,json");\n    }\n\n    function exportCsv() {\n        // Override global, allow only CSV for this action\n        onlyProvides("csv");\n\n        orders = model("order").findAll();\n    }\n}\n
"},"hint":"Use this in an individual controller action to define which formats the action will respond with.\nThis can be used to define provides behavior in individual actions or to override a global setting set with provides in the controller's config().\n\n","returntype":"void","slug":"controller.onlyProvides","parameters":[{"default":"","required":false,"hint":"Formats to instruct the controller to provide. Valid values are `html` (the default), `xml`, `json`, `csv`, `pdf`, and `xls`.","name":"formats","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Name of action, defaults to current.","name":"action","type":"string"}],"availableIn":["controller"],"name":"onlyProvides","tags":{"categoryClass":"providesfunctions","sectionClass":"controller","category":"Provides Functions","section":"Controller"}},{"extended":{"hasExtended":false,"docs":""},"hint":"This method is not designed to be called directly from your code, but provides functionality for dynamic finders such as findOneByEmail()\n\n","returntype":"any","slug":"model.onMissingMethod","parameters":[{"required":true,"name":"missingMethodName","type":"string"},{"required":true,"name":"missingMethodArguments","type":"struct"}],"availableIn":["model"],"name":"onMissingMethod","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    .package("public")\n        // Example URL: /products/1234\n        // Controller:  public.Products\n        .resources("products")\n    .end()\n\n    // Example URL: /users/4321\n    // Controller:  Users\n    .resources(name="users", nested=true)\n        // Calling `package` here is useful to scope nested routes for the `users`\n        // resource into a subfolder.\n        .package("users")\n            // Example URL: /users/4321/profile\n            // Controller:  users.Profiles\n            .resource("profile")\n        .end()\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Scopes the controllers for any routes defined inside its block to a specific subfolder (package) without adding the package name to the URL. This is useful for organizing your controllers in subfolders while keeping the URL structure clean.\n\n","returntype":"struct","slug":"mapper.package","parameters":[{"required":true,"hint":"Name to prepend to child route names.","name":"name","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Subfolder (package) to reference for controllers. This defaults to the value provided for `name`.","name":"package","type":"string"}],"availableIn":["mapper"],"name":"package","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
component extends="app.tests.Test" {\n\n    function packageSetup() {\n        // Run once before any test in this package\n        \n        // Create a test user\n        model("user").new(username="testuser", email="test@example.com").save();\n\n        // Initialize test data\n        application.testConfig = {\n            siteName: "Wheels Test"\n        };\n    }\n\n    function test_User_Creation() {\n        var user = model("user").findOneByUsername("testuser");\n        assert("user eq true");\n    }\n\n    function test_Config_Value() {\n        assert("application.testConfig.siteName eq 'Wheels Test'");\n    }\n}\n
"},"hint":"The packageSetup() function is a callback in Wheels’ legacy testing framework. It runs once before the first test case in the test package. Use it to perform setup tasks that are shared across all tests in the package, such as initializing data, creating test records, or configuring environment settings.\n\n","returntype":"any","slug":"test.packageSetup","parameters":[],"availableIn":["test"],"name":"packageSetup","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
component extends="app.tests.Test" {\n\n    function packageSetup() {\n        // Run once before any test in this package\n        model("user").new(username="testuser", email="test@example.com").save();\n    }\n\n    function packageTeardown() {\n        // Run once after all tests in this package\n\n        // Delete test user\n        var user = model("user").findOneByUsername("testuser");\n        if (user) {\n            user.delete();\n        }\n\n        // Clear test configuration\n        structClear(application.testConfig);\n    }\n\n    function test_User_Exists() {\n        var user = model("user").findOneByUsername("testuser");\n        assert("user eq true");\n    }\n}\n
"},"hint":"The packageTeardown() function is a callback in Wheels’ legacy testing framework. It runs once after the last test case in the test package. Use it to perform cleanup tasks that are shared across all tests in the package, such as deleting test records, resetting application state, or clearing cached data.\n\n","returntype":"any","slug":"test.packageTeardown","parameters":[],"availableIn":["test"],"name":"packageTeardown","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
allAuthors = model("author").findAll(page=1, perPage=25, order="lastName", handle="authorsData");\npaginationData = pagination("authorsData");\n\n#pagination().currentPage#\n#pagination().totalPages#\n#pagination().totalRecords#
"},"hint":"Returns a struct with information about the specificed paginated query.\nThe keys that will be included in the struct are currentPage, totalPages and totalRecords.\n\n","returntype":"struct","slug":"controller.pagination","parameters":[{"default":"query","required":false,"hint":"The handle given to the query to return pagination information for.","name":"handle","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"pagination","tags":{"categoryClass":"paginationfunctions","sectionClass":"controller","category":"Pagination Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
//--------------------------------------------------------------------\n// Example 1: List authors page by page, 25 at a time\n\n// Controller code\nparam name="params.page" type="integer" default="1";\nauthors = model("author").findAll(page=params.page, perPage=25, order="lastName");\n\n// View code\n<ul>\n    <cfoutput query="authors">\n        <li>#EncodeForHtml(firstName)# #EncodeForHtml(lastName)#</li>\n    </cfoutput>\n</ul>\n\n<cfoutput>#paginationLinks(route="authors")#</cfoutput>\n\n\n//--------------------------------------------------------------------\n// Example 2: Using the same model call above, show all authors with a\n// window size of 5\n\n// View code\n<cfoutput>#paginationLinks(route="authors", windowSize=5)#</cfoutput>\n\n\n//--------------------------------------------------------------------\n// Example 3: If more than one paginated query is being run, then you\n// need to reference the correct `handle` in the view\n\n// Controller code\nauthors = model("author").findAll(handle="authQuery", page=5, order="id");\n\n// View code\n<ul>\n    <cfoutput>\n        #paginationLinks(\n            route="authors",\n            handle="authQuery",\n            prependToLink="<li>",\n            appendToLink="</li>"\n        )#\n    </cfoutput>\n</ul>\n\n\n//--------------------------------------------------------------------\n// Example 4: Call to `paginationLinks` using routes\n\n// Route setup in config/routes.cfm\nmapper()\n    .get(name="paginatedCommentListing", pattern="blog/[year]/[month]/[day]/[page]", to="blogs##stats")\n    .get(name="commentListing", pattern="blog/[year]/[month]/[day]", to="blogs##stats")\n.end();\n\n// Controller code\nparam name="params.page" type="integer" default="1";\ncomments = model("comment").findAll(page=params.page, order="createdAt");\n\n// View code\n<ul>\n    <cfoutput>\n        #paginationLinks(\n            route="paginatedCommentListing",\n            year=2009,\n            month="feb",\n            day=10\n        )#\n    </cfoutput>\n</ul>\n
"},"hint":"Builds and returns a string containing links to pages based on a paginated query.\nUses linkTo() internally to build the link, so you need to pass in a route name or a controller/action/key combination.\nAll other linkTo() arguments can be supplied as well, in which case they are passed through directly to linkTo().\nIf you have paginated more than one query in the controller, you can use the handle argument to reference them. (Don't forget to pass in a handle to the findAll() function in your controller first.)\n\n","returntype":"string","slug":"controller.paginationLinks","parameters":[{"default":2,"required":false,"hint":"The number of page links to show around the current page.","name":"windowSize","type":"numeric"},{"default":true,"required":false,"hint":"Whether or not links to the first and last page should always be displayed.","name":"alwaysShowAnchors","type":"boolean"},{"default":" ... ","required":false,"hint":"String to place next to the anchors on either side of the list.","name":"anchorDivider","type":"string"},{"default":false,"required":false,"hint":"Whether or not the current page should be linked to.","name":"linkToCurrentPage","type":"boolean"},{"default":"","required":false,"hint":"String or HTML to be prepended before result.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String or HTML to be appended after result.","name":"append","type":"string"},{"default":"","required":false,"hint":"String or HTML to be prepended before each page number.","name":"prependToPage","type":"string"},{"default":false,"required":false,"name":"addActiveClassToPrependedParent","type":"boolean"},{"default":true,"required":false,"hint":"Whether or not to prepend the prependToPage string on the first page in the list.","name":"prependOnFirst","type":"boolean"},{"default":true,"required":false,"hint":"Whether or not to prepend the prependToPage string on the anchors.","name":"prependOnAnchor","type":"boolean"},{"default":"","required":false,"hint":"String or HTML to be appended after each page number.","name":"appendToPage","type":"string"},{"default":true,"required":false,"hint":"Whether or not to append the appendToPage string on the last page in the list.","name":"appendOnLast","type":"boolean"},{"default":true,"required":false,"hint":"Whether or not to append the appendToPage string on the anchors.","name":"appendOnAnchor","type":"boolean"},{"default":"","required":false,"hint":"Class name for the current page number (if linkToCurrentPage is true, the class name will go on the a element. If not, a span element will be used).","name":"classForCurrent","type":"string"},{"default":"query","required":false,"hint":"The handle given to the query that the pagination links should be displayed for.","name":"handle","type":"string"},{"default":"page","required":false,"hint":"The name of the param that holds the current page number.","name":"name","type":"string"},{"default":false,"required":false,"hint":"Will show a single page when set to true. (The default behavior is to return an empty string when there is only one page in the pagination).","name":"showSinglePage","type":"boolean"},{"default":true,"required":false,"hint":"Decides whether to link the page number as a param or as part of a route. (The default behavior is true).","name":"pageNumberAsParam","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"paginationLinks","tags":{"categoryClass":"linkfunctions","sectionClass":"viewhelpers","category":"Link Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Password Field\n<cfoutput>\n    #passwordField(label="Password", objectName="user", property="password")#\n</cfoutput>\n\n2. Password Field for a Nested Association\n<fieldset>\n    <legend>Passwords</legend>\n    <cfloop from="1" to="#ArrayLen(user.passwords)#" index="i">\n        #passwordField(\n            label="Password ##i#", \n            objectName="user", \n            association="passwords", \n            position=i, \n            property="password"\n        )#\n    </cfloop>\n</fieldset>\n\n3. Custom Label Placement and Error Handling\n<cfoutput>\n    #passwordField(\n        label="Enter Your Password",\n        objectName="user",\n        property="password",\n        labelPlacement="before",\n        errorClass="input-error",\n        prepend="<div class='input-group'>",\n        append="</div>"\n    )#\n</cfoutput>\n
"},"hint":"Builds and returns a string containing a password field form control based on the supplied objectName and property.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.passwordField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"passwordField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Password Field\n<cfoutput>\n    #passwordFieldTag(label="Password", name="password", value="")#\n</cfoutput>\n\n2. Label Placement Before Input\n<cfoutput>\n    #passwordFieldTag(label="Password", name="password", labelPlacement="before")#\n</cfoutput>\n\n3. Wrapping Input with Custom HTML\n<cfoutput>\n    #passwordFieldTag(\n        label="Enter Password",\n        name="password",\n        prepend="<div class='input-group'>",\n        append="</div>"\n    )#\n</cfoutput>\n\n4. Custom Label Decoration\n<cfoutput>\n    #passwordFieldTag(\n        label="Password",\n        name="password",\n        prependToLabel="<strong>",\n        appendToLabel="</strong>"\n    )#\n</cfoutput>\n
"},"hint":"Builds and returns a string containing a password field form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.passwordFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"passwordFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  ghostStory\n    // Example URL: /ghosts/666/stories/616\n    // Controller:  Stories\n    // Action:      update\n    .patch(name="ghostStory", pattern="ghosts/[ghostKey]/stories/[key]", to="stories##update")\n\n    // Route name:  goblins\n    // Example URL: /goblins\n    // Controller:  Goblins\n    // Action:      update\n    .patch(name="goblins", controller="goblins", action="update")\n\n    // Route name:  heartbeat\n    // Example URL: /heartbeat\n    // Controller:  Sessions\n    // Action:      update\n    .patch(name="heartbeat", to="sessions##update")\n\n    // Route name:  usersPreferences\n    // Example URL: /preferences\n    // Controller:  users.Preferences\n    // Action:      update\n    .patch(name="preferences", to="preferences##update", package="users")\n\n    // Route name:  orderShipment\n    // Example URL: /shipments/5432\n    // Controller:  orders.Shipments\n    // Action:      update\n    .patch(\n        name="shipment",\n        pattern="shipments/[key]",\n        to="shipments##update",\n        package="orders"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="subscribers", nested=true)\n        // Route name:  launchSubscribers\n        // Example URL: /subscribers/3209/launch\n        // Controller:  Subscribers\n        // Action:      launch\n        .patch(name="launch", to="subscribers##update", on="collection")\n\n        // Route name:  discontinueSubscriber\n        // Example URL: /subscribers/2251/discontinue\n        // Controller:  Subscribers\n        // Action:      discontinue\n        .patch(name="discontinue", to="subscribers##discontinue", on="member")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Create a route that matches a URL requiring an HTTP PATCH method. We recommend using this matcher to expose actions that update database records.\n\n","returntype":"struct","slug":"mapper.patch","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"patch","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Check if a specific plugin is installed\n<cfif ListFindNoCase("scaffold", pluginNames())>\n    <cfoutput>\n        The Scaffold plugin is installed!\n    </cfoutput>\n<cfelse>\n    <cfoutput>\n        Scaffold plugin is not installed.\n    </cfoutput>\n</cfif>\n\n2. List all installed plugins\n<cfoutput>\nInstalled Plugins: #pluginNames()#\n</cfoutput>\n\n3. Loop through all installed plugins\n<cfloop list="#pluginNames()#" index="plugin">\n    <cfoutput>\n        Plugin: #plugin#<br>\n    </cfoutput>\n</cfloop>\n\n4. Conditional logic based on multiple plugins\n<cfset plugins = pluginNames()>\n\n<cfif ListFindNoCase("scaffold", plugins) AND ListFindNoCase("seo", plugins)>\n    <cfoutput>\n        Both Scaffold and SEO plugins are installed.\n    </cfoutput>\n</cfif>\n
"},"hint":"Returns a list of all installed Wheels plugins in your application. This can be useful if you want to check for the presence of a plugin before calling its functionality, or to display available plugins dynamically.\n\n","returntype":"string","slug":"controller.pluginNames","parameters":[],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"pluginNames","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic pluralization\npluralize("person")\n<!--- Returns: "people" --->\n\n2. Pluralization with count (count = 1, so singular is returned)\npluralize(word="car", count=1)\n<!--- Returns: "1 car" --->\n\n3. Pluralization with count (count = 5, so plural is returned)\npluralize(word="car", count=5)\n<!--- Returns: "5 cars" --->\n\n4. Suppressing the count in the result\npluralize(word="dog", count=3, returnCount=false)\n<!--- Returns: "dogs" --->\n\n5. Irregular plural (child → children)\npluralize("child")\n<!--- Returns: "children" --->\n\n6. Uncountable word stays the same\npluralize("equipment")\n<!--- Returns: "equipment" --->\n\n7. With count and uncountable word\npluralize(word="equipment", count=2)\n<!--- Returns: "2 equipment" --->\n
"},"hint":"Returns the plural form of the passed in word. Can also pluralize a word based on a value passed to the count argument. Wheels stores a list of words that are the same in both singular and plural form (e.g. \"equipment\", \"information\") and words that don't follow the regular pluralization rules (e.g. \"child\" / \"children\", \"foot\" / \"feet\"). Use get(\"uncountables\") / set(\"uncountables\", newList) and get(\"irregulars\") / set(\"irregulars\", newList) to modify them to suit your needs.\n\n","returntype":"string","slug":"controller.pluralize","parameters":[{"required":true,"hint":"The word to pluralize.","name":"word","type":"string"},{"default":"-1","required":false,"hint":"Pluralization will occur when this value is not 1.","name":"count","type":"numeric"},{"default":"true","required":false,"hint":"Will return count prepended to the pluralization when true and count is not -1.","name":"returnCount","type":"boolean"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"pluralize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  widgets\n    // Example URL: /sites/918/widgets\n    // Controller:  Widgets\n    // Action:      create\n    .post(name="widgets", pattern="sites/[siteKey]/widgets", to="widgets##create")\n\n    // Route name:  wadgets\n    // Example URL: /wadgets\n    // Controller:  Wadgets\n    // Action:      create\n    .post(name="wadgets", controller="wadgets", action="create")\n\n    // Route name:  authenticate\n    // Example URL: /oauth/token.json\n    // Controller:  Tokens\n    // Action:      create\n    .post(name="authenticate", pattern="oauth/token.json", to="tokens##create")\n\n    // Route name:  usersPreferences\n    // Example URL: /preferences\n    // Controller:  users.Preferences\n    // Action:      create\n    .post(name="preferences", to="preferences##create", package="users")\n\n    // Route name:  extranetOrders\n    // Example URL: /buy-now/orders\n    // Controller:  extranet.Orders\n    // Action:      create\n    .post(\n        name="orders",\n        pattern="buy-now/orders",\n        to="orders##create",\n        package="extranet"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="customers", nested=true)\n        // Route name:  leadsCustomers\n        // Example URL: /customers/leads\n        // Controller:  Leads\n        // Action:      create\n        .post(name="leads", to="leads##create", on="collection")\n\n        // Route name:  cancelCustomer\n        // Example URL: /customers/3209/cancel\n        // Controller:  Cancellations\n        // Action:      create\n        .post(name="cancel", to="cancellations##create", on="member")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Create a route that matches a URL requiring an HTTP POST method. We recommend using this matcher to expose actions that create database records.\n\n","returntype":"struct","slug":"mapper.post","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPosts`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPosts` generates a pattern of `blog-posts`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"post","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Get the primary key of a simple table\n// For employees table with id as primary key\nkeyName = model("employee").primaryKey();\n// Returns: "id"\n\n2. Alias usage\nkeyName = model("employee").primaryKeys();\n// Returns: "id"\n\n3. Composite primary key table (e.g., order_products with order_id + product_id)\nkeys = model("orderProduct").primaryKey();\n// Returns: "order_id,product_id"\n\n4. Fetching just the first key in a composite set\nfirstKey = model("orderProduct").primaryKey(position=1);\n// Returns: "order_id"\n\n5. Fetching the second key in a composite set\nsecondKey = model("orderProduct").primaryKey(position=2);\n// Returns: "product_id"\n
"},"hint":"Returns the name of the primary key column for the table mapped to a given model. Wheels determines this automatically by introspecting the database. If the table uses a single primary key, the function returns that key’s name as a string. For tables with composite primary keys, the function will return a list of all keys. You can optionally pass in the position argument to retrieve a specific key from a composite set. This function is also available as the alias primaryKeys().\n\n","returntype":"string","slug":"model.primaryKey","parameters":[{"default":0,"required":false,"hint":"If you are accessing a composite primary key, pass the position of a single key to fetch.","name":"position","type":"numeric"}],"availableIn":["model"],"name":"primaryKey","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic auto-incrementing integer primary key\nt.primaryKey(name="id", autoIncrement=true);\n\n2. Primary key with custom type\nt.primaryKey(name="sku", type="string", limit=20);\n\n3. Composite primary keys (order_id + product_id)\nt.primaryKey(name="order_id", type="integer");\nt.primaryKey(name="product_id", type="integer");\n\n4. UUID primary key\nt.primaryKey(name="session_id", type="uuid");\n\n5. Primary key with foreign key reference\nt.primaryKey(name="payment_id", autoIncrement=true);\n
"},"hint":"Used inside migration table definitions to define a primary key for the table. By default, it creates a single-column integer primary key, but you can customize the data type, size, precision, and whether it should auto-increment. If you need composite primary keys, you can call this method multiple times within the same table definition. Additionally, you can configure references to other tables, along with cascading behaviors for updates and deletes. Only available in the migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.primaryKey","parameters":[{"required":true,"name":"name","type":"string"},{"default":"integer","required":false,"name":"type","type":"string"},{"default":"false","required":false,"name":"autoIncrement","type":"boolean"},{"required":false,"name":"limit","type":"numeric"},{"required":false,"name":"precision","type":"numeric"},{"required":false,"name":"scale","type":"numeric"},{"required":false,"name":"references","type":"string"},{"default":"","required":false,"name":"onUpdate","type":"string"},{"default":"","required":false,"name":"onDelete","type":"string"}],"availableIn":["tabledefinition"],"name":"primaryKey","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get the primary key of a simple table\n// Employees table has "id" as primary key\nkeyNames = model("employee").primaryKeys();\n// Returns: "id"\n\n2. Composite primary keys (order_products table with order_id + product_id)\nkeys = model("orderProduct").primaryKeys();\n// Returns: "order_id,product_id"\n\n3. Get only the first key in a composite primary key\nfirstKey = model("orderProduct").primaryKeys(position=1);\n// Returns: "order_id"\n\n4. Get only the second key in a composite primary key\nsecondKey = model("orderProduct").primaryKeys(position=2);\n// Returns: "product_id"\n\n5. Using alias for clarity in multi-key situations\n// This makes it more obvious the table has multiple keys\nkeys = model("orderProduct").primaryKeys();\n// Easier to read than using model("orderProduct").primaryKey()\n
"},"hint":"Alias for primaryKey().\nUse this for better readability when you're accessing multiple primary keys.\n\n","returntype":"string","slug":"model.primaryKeys","parameters":[{"default":0,"required":false,"hint":"If you are accessing a composite primary key, pass the position of a single key to fetch.","name":"position","type":"numeric"}],"availableIn":["model"],"name":"primaryKeys","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Run an action with default behavior (all filters applied)\nresult = processAction("show");\n// Executes the "show" action of the current controller with before/after filters\n\n2. Run an action but only apply "before" filters\nresult = processAction("edit", includeFilters="before");\n// Useful for testing preconditions without running the full action\n\n3. Run an action but only apply "after" filters\nresult = processAction("update", includeFilters="after");\n// Useful for testing cleanup logic that runs post-action\n\n4. Run an action without any filters\nresult = processAction("delete", includeFilters=false);\n// Skips before/after filters, only executes the "delete" action\n\n5. Simulating in a test case\nit("should process the show action without filters", function() {\n    var controller = controller("users");\n    var success = controller.processAction("show", includeFilters=false);\n    expect(success).toBeTrue();\n});\n
"},"hint":"Process the specified action of the controller.\nThis is exposed in the API primarily for testing purposes; you would not usually call it directly unless in the test suite. The optional includeFilters argument allows you to control whether before filters, after filters, or no filters at all should run when invoking the action. By default, all filters execute unless explicitly restricted.\n\n","returntype":"boolean","slug":"controller.processAction","parameters":[{"default":true,"required":false,"hint":"Set to `before` to only execute \"before\" filters, `after` to only execute \"after\" filters or `false` to skip all filters. This argument is generally inherited from the `processRequest` function during unit test execution.","name":"includeFilters","type":"string"}],"availableIn":["controller"],"name":"processAction","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Simple request, returns rendered output as string\nresult = processRequest(params={controller="users", action="show", id=5});\n// Returns: rendered HTML for the users/show action\n\n2. Simulate a POST request\nresult = processRequest(\n    params={controller="users", action="create", name="Alice"},\n    method="post"\n);\n// Returns: rendered output of the create action\n\n3. Get a detailed struct response instead of just body\nresult = processRequest(\n    params={controller="sessions", action="create", email="test@example.com"},\n    method="post",\n    returnAs="struct"\n);\n// Returns struct with keys: body, emails, files, flash, redirect, status, type\n\n4. Automatically roll back database changes\nresult = processRequest(\n    params={controller="orders", action="create", product_id=42},\n    method="post",\n    rollback=true\n);\n// Data is inserted during the request but rolled back afterward\n\n5. Skip all filters\nresult = processRequest(\n    params={controller="users", action="delete", id=10},\n    includeFilters=false\n);\n// Runs delete action without before/after filters\n\n6. Run only "before" filters (useful for testing filter logic)\nresult = processRequest(\n    params={controller="users", action="edit", id=10},\n    includeFilters="before"\n);\n
"},"hint":"Creates a controller and calls an action on it.\nWhich controller and action that's called is determined by the params passed in.\nReturns the result of the request either as a string or in a struct with body, emails, files, flash, redirect, status, and type.\nPrimarily used for testing purposes.\n\n","returntype":"any","slug":"controller.processRequest","parameters":[{"required":true,"hint":"The params struct to use in the request (make sure that at least `controller` and `action` are set).","name":"params","type":"struct"},{"default":"get","required":false,"hint":"The HTTP method to use in the request (`get`, `post` etc).","name":"method","type":"string"},{"default":"","required":false,"hint":"Pass in `struct` to return all information about the request instead of just the final output (`body`).","name":"returnAs","type":"string"},{"default":false,"required":false,"hint":"Pass in `true` to roll back all database transactions made during the request.","name":"rollback","type":"string"},{"default":true,"required":false,"hint":"Set to `before` to only execute \"before\" filters, `after` to only execute \"after\" filters or `false` to skip all filters.","name":"includeFilters","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"processRequest","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Get all properties for a user object\nuser = model("user").findByKey(1);\nprops = user.properties();\n\n2. Exclude nested/associated properties\nuser = model("user").findByKey(1);\nprops = user.properties(returnIncluded=false);\n\n3. Iterate through properties\nuser = model("user").findByKey(2);\nprops = user.properties();\nfor (key in props) {\n    writeOutput("#key#: #props[key]#<br>");\n}\n\n4. Convert properties to JSON for API output\nuser = model("user").findByKey(3);\nprops = user.properties(returnIncluded=false);\njsonData = serializeJSON(props);\n
"},"hint":"Returns a structure containing all the properties of a model object, where the keys are the property (column) names and the values are the current values for that object. This is useful when you want to inspect all the attributes of a record at once, serialize data, or debug object state. By default, properties() includes nested or associated properties (such as related objects). You can control this behavior using the returnIncluded argument to exclude them if you only want the direct properties of the object.\n\n","returntype":"struct","slug":"model.properties","parameters":[{"default":true,"required":false,"hint":"Whether to return nested properties or not.","name":"returnIncluded","type":"boolean"}],"availableIn":["model"],"name":"properties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Tell Wheels that when we are referring to `firstName` in the CFML code, it should translate to the `STR_USERS_FNAME` column when interacting with the database instead of the default (which would be the `firstname` column)\nproperty(name="firstName", column="STR_USERS_FNAME");\n\n2. Tell Wheels that when we are referring to `fullName` in the CFML code, it should concatenate the `STR_USERS_FNAME` and `STR_USERS_LNAME` columns\nproperty(name="fullName", sql="STR_USERS_FNAME + ' ' + STR_USERS_LNAME");\n\n3. Tell Wheels that when displaying error messages or labels for form fields, we want to use `First name(s)` as the label for the `STR_USERS_FNAME` column\nproperty(name="firstName", label="First name(s)");\n\n4. Tell Wheels that when creating new objects, we want them to be auto-populated with a `firstName` property of value `Dave`\nproperty(name="firstName", defaultValue="Dave");\n\n5. Exclude property from SELECT queries\n// Useful for virtual/computed properties you don’t want fetched from the DB\nproperty(name=\"tempValue\", select=false);\n\n6. Override data type explicitly\nproperty(name=\"isActive\", dataType=\"boolean\");\n
"},"hint":"Lets you customize how model properties map to database columns or SQL expressions. By default, Wheels automatically maps a model’s property name to the column with the same name in the table. However, when your database uses non-standard column names, calculated values, or requires custom behavior, you can use property() to override the default mapping.\n\n","returntype":"void","slug":"model.property","parameters":[{"required":true,"hint":"The name that you want to use for the column or SQL function result in the CFML code.","name":"name","type":"string"},{"default":"","required":false,"hint":"The name of the column in the database table to map the property to.","name":"column","type":"string"},{"default":"","required":false,"hint":"An SQL expression to use to calculate the property value.","name":"sql","type":"string"},{"default":"","required":false,"hint":"A custom label for this property to be referenced in the interface and error messages.","name":"label","type":"string"},{"required":false,"hint":"A default value for this property.","name":"defaultValue","type":"string"},{"default":"true","required":false,"hint":"Whether to include this property by default in SELECT statements","name":"select","type":"boolean"},{"default":"char","required":false,"hint":"Specify the column dataType for this property","name":"dataType","type":"string"},{"required":false,"hint":"Enable / disable automatic validations for this property.","name":"automaticValidations","type":"boolean"}],"availableIn":["model"],"name":"property","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\nuser = model("user").new();\nisBlank = user.propertyIsBlank("firstName"); // returns true if firstName is not set\n\n2. Property exists but is empty\nuser = model("user").new(firstName="");\nisBlank = user.propertyIsBlank("firstName"); // true\n\n3. Property exists with value\nuser = model("user").new(firstName="Joe");\nisBlank = user.propertyIsBlank("firstName"); // false\n\n4. Checking property that doesn’t exist on the model\nisBlank = user.propertyIsBlank("nonexistentProperty"); // true\n\n5. Using in validation logic\nif (user.propertyIsBlank("email")) {\n    writeOutput("Email is required.");\n}\n
"},"hint":"Returns true if the specified property doesn't exist on the model or is an empty string.\nThis method is the inverse of propertyIsPresent().\n\n","returntype":"boolean","slug":"model.propertyIsBlank","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"propertyIsBlank","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Property exists with a value\nemployee = model("employee").new();\nemployee.firstName = "Dude";\nwriteOutput(employee.propertyIsPresent("firstName")); // true\n\n2. Property exists but is blank\nemployee.firstName = "";\nwriteOutput(employee.propertyIsPresent("firstName")); // false\n\n3. Property does not exist on the model\nwriteOutput(employee.propertyIsPresent("nonexistentProperty")); // false\n\n4. Conditional logic\nif (!employee.propertyIsPresent("email")) {\n    writeOutput("Email is required.");\n}\n
"},"hint":"Returns true if the specified property exists on the model and is not a blank string. This is the inverse of propertyIsBlank() which checks that a property is either missing or empty.\n\n","returntype":"boolean","slug":"model.propertyIsPresent","parameters":[{"required":true,"hint":"Name of property to inspect.","name":"property","type":"string"}],"availableIn":["model"],"name":"propertyIsPresent","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Get property names for the User model\npropNames = model("user").propertyNames();\nwriteOutput(propNames);\n\n2. Loop through property names\nfor (prop in listToArray(model("employee").propertyNames())) {\n    writeOutput("Property: #prop#<br>");\n}\n\n3. Check if a property exists in the list\nif (listFindNoCase(model("order").propertyNames(), "totalAmount")) {\n    writeOutput("Order model has a totalAmount property.");\n}\n\n4. Including calculated properties\n// In the model configuration:\nproperty(name="fullName", sql="firstName + ' ' + lastName");\n\n// propertyNames() will now include "fullName"\nwriteOutput(model("user").propertyNames());\n
"},"hint":"Returns a list of all property names associated with a model. The list is ordered by the columns’ ordinal positions as they exist in the underlying database table. In addition to actual table columns, the list also includes any calculated properties defined through the property(), method, which may be derived from SQL expressions or mapped column names. This is useful when you need to dynamically work with all of a model’s attributes without hardcoding them, such as generating dynamic forms, building custom serializers, or inspecting ORM mappings.\n\n","returntype":"string","slug":"model.propertyNames","parameters":[],"availableIn":["model"],"name":"propertyNames","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
// In `app/models/User.cfc`, `firstName` and `lastName` cannot be changed through mass assignment operations like `updateAll()`.\nfunction config(){\n\tprotectedProperties("firstName,lastName");\n}\n
"},"hint":"Used to protect one or more model properties from being set or modified through mass assignment operations. Mass assignment occurs when values are assigned to a model in bulk, such as through create(), update(), or updateAll() using a struct of data. By marking certain properties as protected, you can prevent accidental or malicious changes to sensitive fields (such as id, role, or passwordHash). This method is typically called in the model’s config() function to define rules that apply across the entire model.\n\n","returntype":"void","slug":"model.protectedProperties","parameters":[{"default":"","required":false,"hint":"Property name (or list of property names) that are not allowed to be altered through mass assignment.","name":"properties","type":"string"}],"availableIn":["model"],"name":"protectedProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Protect all POST requests globally\n// In app/controllers/Controller.cfc\nfunction config() {\n    protectsFromForgery();\n}
"},"hint":"Tells Wheels to protect POSTed requests from CSRF vulnerabilities.\nInstructs the controller to verify that params.authenticityToken or X-CSRF-Token HTTP header is provided along with the request containing a valid authenticity token.\nCall this method within a controller's config method, preferably the base Controller.cfc file, to protect the entire application.\n\n","returntype":"any","slug":"controller.protectsFromForgery","parameters":[{"default":"exception","required":false,"hint":"How to handle invalid authenticity token checks. Valid values are `error` (throws a `Wheels.InvalidAuthenticityToken` error), `abort` (aborts the request silently and sends a blank response to the client), and `ignore` (ignores the check and lets the request proceed).","name":"with","type":"string"},{"default":"","required":false,"hint":"List of actions that this check should only run on. Leave blank for all.","name":"only","type":"string"},{"default":"","required":false,"hint":"List of actions that this check should be omitted from running on. Leave blank for no exceptions.","name":"except","type":"string"}],"availableIn":["controller"],"name":"protectsFromForgery","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Provide HTML, XML, and JSON responses\nfunction config() {\n    provides("html,xml,json");\n}\n\n2. Provide only JSON and CSV\nfunction config() {\n    provides("json,csv");\n}\n\n3. Default behavior (HTML only)\nfunction config() {\n    provides(); // equivalent to provides("html")\n}\n\n4. Handling requested format in the action\nfunction show() {\n    // Wheels automatically detects the requested format and renders accordingly\n    renderwith(data=model("user").findByKey(params.id));\n}\n
"},"hint":"The `provides()` function defines the response formats that a controller can return. Clients can request a specific format in three ways: by using a URL parameter called `format` (e.g., `?format=json`), by appending the format as an extension to the URL (e.g., `/users/1.json`) when URL rewriting is enabled, or by specifying the desired format in the `Accept` header of the HTTP request. By defining the supported formats, you ensure that your controller can automatically render the response in the requested format, such as HTML, JSON, XML, CSV, PDF, or XLS. If no format is requested or supported, the controller defaults to HTML.\n\n","returntype":"void","slug":"controller.provides","parameters":[{"default":"","required":false,"hint":"Formats to instruct the controller to provide. Valid values are `html` (the default), `xml`, `json`, `csv`, `pdf`, and `xls`.","name":"formats","type":"string"}],"availableIn":["controller"],"name":"provides","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Route name:  ghostStory\n    // Example URL: /ghosts/666/stories/616\n    // Controller:  Stories\n    // Action:      update\n    .put(name="ghostStory", pattern="ghosts/[ghostKey]/stories/[key]", to="stories##update")\n\n    // Route name:  goblins\n    // Example URL: /goblins\n    // Controller:  Goblins\n    // Action:      update\n    .put(name="goblins", controller="goblins", action="update")\n\n    // Route name:  heartbeat\n    // Example URL: /heartbeat\n    // Controller:  Sessions\n    // Action:      update\n    .put(name="heartbeat", to="sessions##update")\n\n    // Route name:  usersPreferences\n    // Example URL: /preferences\n    // Controller:  users.Preferences\n    // Action:      update\n    .put(name="preferences", to="preferences##update", package="users")\n\n    // Route name:  orderShipment\n    // Example URL: /shipments/5432\n    // Controller:  orders.Shipments\n    // Action:      update\n    .put(\n        name="shipment",\n        pattern="shipments/[key]",\n        to="shipments##update",\n        package="orders"\n    )\n\n    // Example scoping within a nested resource\n    .resources(name="subscribers", nested=true)\n        // Route name:  launchSubscribers\n        // Example URL: /subscribers/3209/launch\n        // Controller:  Subscribers\n        // Action:      launch\n        .put(name="launch", to="subscribers##update", on="collection")\n\n        // Route name:  discontinueSubscriber\n        // Example URL: /subscribers/2251/discontinue\n        // Controller:  Subscribers\n        // Action:      discontinue\n        .put(name="discontinue", to="subscribers##discontinue", on="member")\n    .end()\n.end();\n\n</cfscript>
"},"hint":"Create a route that matches a URL requiring an HTTP PUT method. We recommend using this matcher to expose actions that update database records. This method is provided as a convenience for when you really need to support the PUT verb. Consider using the patch matcher instead of this one.\n\n","returntype":"struct","slug":"mapper.put","parameters":[{"required":false,"hint":"Camel-case name of route to reference when build links and form actions (e.g., `blogPost`).","name":"name","type":"string"},{"required":false,"hint":"Overrides the URL pattern that will match the route. The default value is a dasherized version of `name` (e.g., a `name` of `blogPost` generates a pattern of `blog-post`).","name":"pattern","type":"string"},{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Map the route to a given controller. This must be passed along with the `action` argument.","name":"controller","type":"string"},{"required":false,"hint":"Map the route to a given action within the `controller`. This must be passed along with the `controller` argument.","name":"action","type":"string"},{"required":false,"hint":"Indicates a subfolder that the controller will be referenced from (but not added to the URL pattern). For example, if you set this to `admin`, the controller will be located at `admin/YourController.cfc`, but the URL path will not contain `admin/`.","name":"package","type":"string"},{"required":false,"hint":"If this route is within a nested resource, you can set this argument to `member` or `collection`. A `member` route contains a reference to the resource's `key`, while a `collection` route does not.","name":"on","type":"string"},{"required":false,"hint":"Redirect via 302 to this URL when this route is matched. Has precedence over controller/action. Use either an absolute link like `/about/`, or a full canonical link.","name":"redirect","type":"string"}],"availableIn":["mapper"],"name":"put","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic radio buttons for gender\n<cfoutput>\n<fieldset>\n    <legend>Gender</legend>\n    #radioButton(objectName="user", property="gender", tagValue="m", label="Male")#<br>\n    #radioButton(objectName="user", property="gender", tagValue="f", label="Female")#\n</fieldset>\n</cfoutput>\n\n2. Radio buttons for nested association (committee members)\n<cfoutput>\n<cfloop from="1" to="#ArrayLen(committee.members)#" index="i">\n    <div>\n        <h3>#committee.members[i].fullName#:</h3>\n        <div>\n            #radioButton(\n                objectName="committee",\n                association="members",\n                position=i,\n                property="gender",\n                tagValue="m",\n                label="Male"\n            )#<br>\n            #radioButton(\n                objectName="committee",\n                association="members",\n                position=i,\n                property="gender",\n                tagValue="f",\n                label="Female"\n            )#\n        </div>\n    </div>\n</cfloop>\n</cfoutput>\n\n3. Custom HTML wrapping and label placement\n#radioButton(\n    objectName="user",\n    property="subscription",\n    tagValue="premium",\n    label="Premium Plan",\n    prepend="<div class='radio-wrapper'>",\n    append="</div>",\n    labelPlacement="aroundRight"\n)#\n
"},"hint":"Generates an HTML radio button for a form, based on a model object’s property. It can handle simple properties as well as nested properties through associations, making it ideal for forms that work with both individual objects and collections. You can customize the radio button with additional attributes, labels, and error handling options. It automatically reflects the object’s current property value, so if the property matches the tagValue, the radio button will be marked as checked. This function helps you build dynamic forms safely and easily, with support for encoding to prevent XSS attacks, error highlighting, and custom HTML wrapping.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.radioButton","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"required":false,"hint":"The value of the radio button when selected.","name":"tagValue","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"radioButton","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic radio buttons for gender\n<cfoutput>\n<fieldset>\n    <legend>Gender</legend>\n    #radioButtonTag(name="gender", value="m", label="Male", checked=true)#<br>\n    #radioButtonTag(name="gender", value="f", label="Female")#\n</fieldset>\n</cfoutput>\n\n2. Label before radio button\n#radioButtonTag(name="subscription", value="premium", label="Premium Plan", labelPlacement="before")#\n\n3. Custom HTML wrappers\n#radioButtonTag(\n    name="newsletter",\n    value="yes",\n    label="Subscribe",\n    prepend="<div class='radio-wrapper'>",\n    append="</div>",\n    labelPlacement="aroundRight"\n)#\n
"},"hint":"Generates a standard HTML <input type=\"radio\"> element based on the supplied name and value. Unlike radioButton(), this function works directly with form tags rather than binding to a model object. It is useful for simple forms or when you need fine-grained control over the HTML attributes. You can customize the radio button with labels, label placement, HTML wrapping, and encoding to prevent XSS attacks. The generated radio button will be marked as checked if the checked argument is true.\n\n","returntype":"string","slug":"controller.radioButtonTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"required":true,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":false,"required":false,"hint":"Whether or not to check the radio button by default.","name":"checked","type":"boolean"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"radioButtonTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Testing for a specific exception\n// Assume updateUser() should throw an error if email is invalid\nerrorType = raised('model("user").updateUser({email="invalid-email"})');\nassert("errorType eq Wheels.InvalidEmailException");\n\n2. Using raised() in a test case\nfunction testInvalidPassword() {\n    var errorType = raised('model("user").login(username="jdoe", password="wrong")');\n    writeOutput("Caught error type: " & errorType);\n    // Output: Caught error type: Wheels.InvalidPassword\n}\n\n3. Catching any error\nvar errorType = raised('1 / 0'); // Division by zero\nwriteOutput(errorType);\n
"},"hint":"Used in legacy Wheels testing to catch errors or exceptions raised by a given CFML expression. It evaluates the expression and, if an error occurs, returns the type of the error. This is especially useful when writing tests to ensure that specific operations correctly trigger exceptions under invalid or unexpected conditions. By using raised(), you can assert that your code behaves safely and predictably when encountering errors.\n\n","returntype":"string","slug":"test.raised","parameters":[{"required":true,"hint":"String containing CFML expression to evaluate","name":"expression","type":"string"}],"availableIn":["test"],"name":"raised","tags":{"categoryClass":"testingfunctions","sectionClass":"testmodel","category":"Testing Functions","section":"Test Model"}},{"extended":{"hasExtended":true,"docs":"
1. Redirect to an action after saving\nif (user.save()) {\n    redirectTo(action="saveSuccessful");\n}\n\n2. Redirect to a secure checkout page with parameters\nredirectTo(\n    controller="checkout",\n    action="start",\n    params="type=express",\n    protocol="https"\n);\n\n3. Redirect to a named route and pass a route parameter\nredirectTo(route="profile", screenName="Joe");\n\n4. Redirect back to the referring page\nredirectTo(back=true);\n\n5. Redirect to an external URL\nredirectTo(url="https://example.com/welcome");\n
"},"hint":"Used to redirect the browser to another page, action, controller, route, or back to the referring page. Internally, it uses Wheels’ URLFor() function to construct the URL and the <cflocation> tag (or equivalent in your CFML engine) to perform the actual redirect. You can redirect to internal routes or controllers, pass keys and query parameters, include anchors, override protocol, host, or port, and even delay the redirect until after your action code executes. This function ensures URLs are safely encoded and properly formatted for the redirect.\n\n","returntype":"void","slug":"controller.redirectTo","parameters":[{"default":false,"required":false,"hint":"Set to `true` to redirect back to the referring page.","name":"back","type":"boolean"},{"default":false,"required":false,"hint":"See documentation for your CFML engine's implementation of `cflocation`.","name":"addToken","type":"boolean"},{"default":302,"required":false,"hint":"See documentation for your CFML engine's implementation of `cflocation`.","name":"statusCode","type":"numeric"},{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"name":"method","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: `wheels=cool&x=y`). Please note that Wheels uses the `&` and `=` characters to split the parameters and encode them properly for you. However, if you need to pass in `&` or `=` as part of the value, then you need to encode them (and only them), example: `a=cats%26dogs%3Dtrouble!&b=1`.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If `true`, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":"","required":false,"hint":"Redirect to an external URL.","name":"url","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to delay the redirection until after the rest of your action code has executed.","name":"delay","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"redirectTo","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Rerun a specific migration version\nresult = redoMigration(version="202509250915");\nwriteOutput(result); // Returns status or log of the migration rerun\n\n2. Using redoMigration in a script for testing\nif (environment() == "development") {\n    redoMigration(version="202509250920");\n}\n\n3. Rerun latest migration (if version not specified)\nresult = redoMigration();\nwriteOutput(result);\n
"},"hint":"Allows you to rerun a specific database migration version. This can be useful for testing migrations, correcting issues in a migration, or resetting a schema change during development. While it can be called directly from your application code, it is generally recommended to use this function via the CommandBox CLI or the Wheels GUI migration interface, as these provide safer execution and logging.\n\n","returntype":"string","slug":"migrator.redoMigration","parameters":[{"default":"","required":false,"hint":"The Database schema version to rerun","name":"version","type":"string"}],"availableIn":["migrator"],"name":"redoMigration","tags":{"categoryClass":"generalfunctions","sectionClass":"migrator","category":"General Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic reference column\nt.references("userId");\n\n2. Multiple references with nulls allowed\nt.references(referenceNames="userId,orderId", allowNull=true);\n\n3. Reference with default value\nt.references(referenceNames="statusId", default=1);\n\n4. Polymorphic reference (used in polymorphic associations)\nt.references(referenceNames="referenceableId", polymorphic=true);\n\n5. Custom foreign key actions\nt.references(\n    referenceNames="customerId",\n    onUpdate="CASCADE",\n    onDelete="SET NULL"\n);\n
"},"hint":"Used when defining a table schema to add reference columns that act as foreign keys, linking the table to other tables in the database. It automatically creates integer columns for the references and sets up foreign key constraints, helping maintain referential integrity. You can customize the behavior of these reference columns, including whether they allow nulls, default values, or support polymorphic associations. You can also define actions for ON UPDATE and ON DELETE events.\n\n","returntype":"any","slug":"tabledefinition.references","parameters":[{"required":true,"name":"referenceNames","type":"string"},{"required":false,"name":"default","type":"string"},{"default":"false","required":false,"name":"allowNull","type":"boolean"},{"default":"false","required":false,"name":"polymorphic","type":"boolean"},{"default":"true","required":false,"name":"foreignKey","type":"boolean"},{"default":"","required":false,"name":"onUpdate","type":"string"},{"default":"","required":false,"name":"onDelete","type":"string"}],"availableIn":["tabledefinition"],"name":"references","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get an object, call a method on it that could potentially change values, and then reload the values from the database\nemployee = model("employee").findByKey(params.key);\nemployee.someCallThatChangesValuesInTheDatabase();\nemployee.reload();
"},"hint":"Refreshes the property values of a model object from the database. This is useful when an object’s values might have changed in the database due to other operations or external processes. By calling reload(), you ensure that your object reflects the current state of the corresponding database record.\n\n","returntype":"void","slug":"model.reload","parameters":[],"availableIn":["model"],"name":"reload","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Remove a single column from a table\nremoveColumn(table="users", columnName="middleName");\n\n2. Remove a foreign key reference column\nremoveColumn(table="orders", referenceName="customerId");\n\n3. Remove multiple columns in separate calls\nremoveColumn(table="products", columnName="oldPrice");\nremoveColumn(table="products", columnName="discountRate");\n
"},"hint":"Used to delete a column from a database table within a migration CFC. This is useful when you need to remove obsolete or incorrectly added columns during schema evolution. Optionally, you can also remove a reference column by specifying its referenceName. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.removeColumn","parameters":[{"required":true,"hint":"The table containing the column to remove","name":"table","type":"string"},{"default":"","required":false,"hint":"The column name to remove","name":"columnName","type":"string"},{"default":"","required":false,"hint":"optional reference name","name":"referenceName","type":"string"}],"availableIn":["migration"],"name":"removeColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Remove an index from the members table\nremoveIndex(table="members", indexName="members_username");\n\n2. Remove an index from the orders table\nremoveIndex(table="orders", indexName="orders_createdAt_idx");\n\n3. Remove multiple indexes in separate calls\nremoveIndex(table="products", indexName="products_name_idx");\nremoveIndex(table="products", indexName="products_category_idx");\n
"},"hint":"Used to delete an index from a database table within a migration CFC. Indexes are typically added to improve query performance, but there are scenarios where an index becomes unnecessary or needs to be replaced. Using removeIndex() allows you to safely remove an index while maintaining database integrity. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.removeIndex","parameters":[{"required":true,"hint":"The table name to perform the index operation on","name":"table","type":"string"},{"required":true,"hint":"the name of the index to remove","name":"indexName","type":"string"}],"availableIn":["migration"],"name":"removeIndex","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Remove a specific record by ID\nremoveRecord(table="users", where="id = 42");\n\n2. Remove multiple records matching a condition\nremoveRecord(table="orders", where="status = 'cancelled'");\n\n3. Remove all records from a table (use with caution)\nremoveRecord(table="temporary_data", where="1=1");\n
"},"hint":"Used to delete specific records from a database table within a migration CFC. This is useful when you need to clean up obsolete data, remove test data, or correct records as part of a schema migration. You can optionally provide a where clause to target specific rows. If no where clause is provided, the behavior depends on the database; usually, no records are removed unless explicitly specified. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.removeRecord","parameters":[{"required":true,"hint":"The table name to remove the record from","name":"table","type":"string"},{"default":"","required":false,"hint":"The where clause, i.e id = 123","name":"where","type":"string"}],"availableIn":["migration"],"name":"removeRecord","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Rename a column in the users table\nrenameColumn(table="users", columnName="username", newColumnName="user_name");\n\n2. Rename a column in the orders table\nrenameColumn(table="orders", columnName="createdAt", newColumnName="order_created_at");\n\n3. Rename multiple columns in separate migration calls\nrenameColumn(table="products", columnName="oldPrice", newColumnName="price_old");\nrenameColumn(table="products", columnName="discountRate", newColumnName="discount_percent");\n
"},"hint":"Used to change the name of an existing column in a database table within a migration CFC. This is useful when you need to standardize column names, correct naming mistakes, or improve clarity in your database schema. Renaming a column preserves the existing data and column type while updating the schema. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.renameColumn","parameters":[{"required":true,"hint":"The table containing the column to rename","name":"table","type":"string"},{"required":true,"hint":"The column name to rename","name":"columnName","type":"string"},{"required":true,"hint":"The new column name","name":"newColumnName","type":"string"}],"availableIn":["migration"],"name":"renameColumn","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Rename the users table\nrenameTable(oldName="users", newName="app_users");\n\n2. Rename the orders table\nrenameTable(oldName="orders", newName="customer_orders");\n\n3. Rename multiple tables in separate migration calls\nrenameTable(oldName="products_old", newName="products");\nrenameTable(oldName="temp_data", newName="archived_data");\n
"},"hint":"Used to change the name of an existing database table within a migration CFC. This is helpful when you want to standardize table names, correct naming mistakes, or improve clarity in your database schema. This operation preserves all the existing data, indexes, and constraints in the table while updating its name. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.renameTable","parameters":[{"required":true,"hint":"Name the old table","name":"oldName","type":"string"},{"required":true,"hint":"New name for the table","name":"newName","type":"string"}],"availableIn":["migration"],"name":"renameTable","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Render an empty page with default status (200 OK)\nrenderNothing();\n\n2. Render nothing with a 204 No Content status\nrenderNothing(status="204");\n\n3. Use renderNothing in an API endpoint after deleting a resource\nfunction deleteResource() {\n    resource = model("resource").findByKey(params.id);\n    resource.delete();\n    renderNothing(status="204");\n}\n
"},"hint":"Instructs the controller to render an empty response when an action completes. Unlike using cfabort, which stops request processing immediately, renderNothing() ensures that any after filters associated with the action still execute. You can optionally provide an HTTP status code to indicate the type of response being returned. This is useful for APIs or endpoints that need to signal a specific status without returning a body.\n\n","returntype":"void","slug":"controller.renderNothing","parameters":[{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderNothing","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render a partial in the current controller's view folder\nrenderPartial("comment");\n\n2. Render a partial from the shared folder\nrenderPartial("/shared/comment");\n\n3. Render a partial without a layout\nrenderPartial(partial="/shared/comment", layout=false);\n\n4. Render a partial and return it as a string\ncommentHtml = renderPartial(partial="comment", returnAs="string");\n\n5. Render a partial with caching for 15 minutes\nrenderPartial(partial="comment", cache=15);\n\n6. Render a partial with a custom HTTP status code\nrenderPartial(partial="comment", status="202");\n
"},"hint":"Instructs the controller to render a partial view when an action completes. Partials are reusable view fragments, typically prefixed with an underscore (e.g., _comment.cfm). This function allows you to render these fragments either directly to the client or capture them as a string for further processing. You can control caching, layouts, HTTP status codes, and data-loading behavior, making it flexible for both full-page updates and AJAX responses.\n\n","returntype":"any","slug":"controller.renderPartial","parameters":[{"required":true,"hint":"The name of the partial file to be used. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Do not include the partial filename's underscore and file extension.","name":"partial","type":"string"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"string"},{"default":"","required":false,"hint":"Set to `string` to return the result instead of automatically sending it to the client.","name":"returnAs","type":"string"},{"default":true,"required":false,"hint":"Name of a controller function to load data from.","name":"dataFunction","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderPartial","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render a simple message\nrenderText("Done!");\n\n2. Render serialized product data as JSON\nproducts = model("product").findAll();\nrenderText(SerializeJson(products));\n\n3. Render a message with a custom HTTP status code\nrenderText(text="Unauthorized access", status=401);\n\n4. Use in an API endpoint\nfunction checkStatus() {\n    if (someCondition()) {\n        renderText(text="OK", status=200);\n    } else {\n        renderText(text="Error", status=500);\n    }\n}\n
"},"hint":"Instructs the controller to output plain text as the response when an action completes. Unlike rendering a view or partial, this sends the specified text directly to the client. This is especially useful for APIs, AJAX responses, or simple status messages. You can also provide an HTTP status code to control the response status.\n\n","returntype":"void","slug":"controller.renderText","parameters":[{"default":"","required":false,"hint":"The text to render.","name":"text","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"any"}],"availableIn":["controller"],"name":"renderText","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render a view page for a different action within the same controller.\nrenderView(action="edit");\n\n2. Render a view page for a different action within a different controller.\nrenderView(controller="blog", action="new");\n\n3. Another way to render the blog/new template from within a different controller.\nrenderView(template="/blog/new");\n\n4. Render the view page for the current action but without a layout and cache it for 60 minutes.\nrenderView(layout=false, cache=60);\n\n5. Load a layout from a different folder within `views`.\nrenderView(layout="/layouts/blog");\n\n6. Don't render the view immediately but rather return and store in a variable for further processing.\nmyView = renderView(returnAs="string");\n
"},"hint":"Instructs the controller which view template and layout to render when it's finished processing the action.\nNote that when passing values for controller and / or action, this function does not execute the actual action but rather just loads the corresponding view template.\n\n","returntype":"any","slug":"controller.renderView","parameters":[{"default":"[runtime expression]","required":false,"hint":"Controller to include the view page for.","name":"controller","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Action to include the view page for.","name":"action","type":"string"},{"default":"","required":false,"hint":"A specific template to render. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder.","name":"template","type":"string"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"any"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"Set to `string` to return the result instead of automatically sending it to the client.","name":"returnAs","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to hide the debug information at the end of the output. This is useful, for example, when you're testing XML output in an environment where the global setting for `showDebugInformation` is `true`.","name":"hideDebugInformation","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderView","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Render all products in the requested format (json, xml, etc.)\nproducts = model("product").findAll();\nrenderWith(products);\n\n2. Render a JSON error message with a 403 status code\nmsg = {\n    "status" : "Error",\n    "message": "Not Authenticated"\n};\nrenderWith(data=msg, status=403);\n\n3. Render with a custom layout\nproducts = model("product").findAll();\nrenderWith(data=products, layout="/layouts/api");\n\n4. Render a view template from a different controller\ndata = model("order").findAll();\nrenderWith(data=data, controller="orders", action="list");\n\n5. Capture the output as a string instead of sending it to the client\noutput = renderWith(data=products, returnAs="string");\n
"},"hint":"Instructs the controller to render the given data in the format requested by the client. If the requested format is json or xml, Wheels automatically converts the data into the appropriate format. For other formats—or to override automatic formatting—you can create a view template matching the requested format, such as nameofaction.json.cfm, nameofaction.xml.cfm, or nameofaction.pdf.cfm. This function is especially useful in APIs, AJAX endpoints, or situations where you need to respond dynamically in multiple formats based on client preferences. You can also control caching, layout, HTTP status codes, and whether to return the result as a string for further processing.\n\n","returntype":"any","slug":"controller.renderWith","parameters":[{"required":true,"hint":"Data to format and render.","name":"data","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Controller to include the view page for.","name":"controller","type":"string"},{"default":"[runtime expression]","required":false,"hint":"Action to include the view page for.","name":"action","type":"string"},{"default":"","required":false,"hint":"A specific template to render. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder.","name":"template","type":"string"},{"default":"","required":false,"hint":"The layout to wrap the content in. Prefix with a leading slash (`/`) if you need to build a path from the root `views` folder. Pass `false` to not load a layout at all.","name":"layout","type":"any"},{"default":"","required":false,"hint":"Number of minutes to cache the content for.","name":"cache","type":"any"},{"default":"","required":false,"hint":"Set to `string` to return the result instead of automatically sending it to the client.","name":"returnAs","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to hide the debug information at the end of the output. This is useful, for example, when you're testing XML output in an environment where the global setting for `showDebugInformation` is `true`.","name":"hideDebugInformation","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Force request to return with specific HTTP status code.","name":"status","type":"string"}],"availableIn":["controller"],"name":"renderWith","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
// alternating row colors and shrinking emphasis\n<cfoutput query="employees" group="departmentId">\n\t<div class="#cycle(values="even,odd", name="row")#">\n\t\t<ul>\n\t\t\t<cfoutput>\n\t\t\t\trank = cycle(values="president,vice-president,director,manager,specialist,intern", name="position")>\n\t\t\t\t<li class="#rank#">#categories.categoryName#</li>\n\t\t\t\tresetCycle("emphasis")>\n\t\t\t</cfoutput>\n\t\t</ul>\n\t</div>\n</cfoutput>
"},"hint":"Rsets a named cycle, allowing it to start from the first value the next time it is called. In Wheels, cycle() is often used to alternate values in a repeated pattern, such as CSS classes for table rows, positions, or emphasis levels. By calling resetCycle(), you ensure that the cycle begins again from its initial value, which is useful when looping through nested structures or when a new grouping starts.\n\n","returntype":"void","slug":"controller.resetCycle","parameters":[{"default":"default","required":false,"hint":"The name of the cycle to reset.","name":"name","type":"string"}],"availableIn":["controller"],"name":"resetCycle","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // With default arguments\n    .resource("checkout")\n\n    // Point auth URL to controller at `controllers/sessions/Auth.cfc`\n    .resource(name="auth", controller="sessions.auth")\n\n    // Limited list of routes generated by `only` argument.\n    .resource(name="profile", only="show,edit,update")\n\n    // Limited list of routes generated by `except` argument.\n    .resource(name="cart", except="new,create,edit,delete")\n\n    // Nested resource\n    .resource(name="preferences", only="index", nested=true)\n      .get(name="editPassword", to="passwords##edit")\n      .patch(name="password", to="passwords##update")\n\n      .resources("foods")\n    .end()\n\n    // Overridden `path`\n    .resource(name="blogPostOptions", path="blog-post/options")\n.end();\n\n</cfscript>
"},"hint":"Create a group of routes that exposes actions for manipulating a singular resource. A singular resource exposes URL patterns for the entire CRUD lifecycle of a single entity (show, new, create, edit, update, and delete) without exposing a primary key in the URL. Usually this type of resource represents a singleton entity tied to the session, application, or another resource (perhaps nested within another resource). If you need to generate routes for manipulating a collection of resources with a primary key in the URL, see the resources mapper method.\n\n","returntype":"struct","slug":"mapper.resource","parameters":[{"required":true,"hint":"Camel-case name of resource to reference when build links and form actions. This is typically a singular word (e.g., `profile`).","name":"name","type":"string"},{"default":false,"required":false,"hint":"Whether or not additional calls will be nested within this resource.","name":"nested","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Override URL path representing this resource. Default is a dasherized version of `name` (e.g., `blogPost` generates a path of `blog-post`).","name":"path","type":"string"},{"required":false,"hint":"Override name of the controller used by resource. This defaults to a pluralized version of `name`.","name":"controller","type":"string"},{"required":false,"hint":"Override singularize() result in plural resources.","name":"singular","type":"string"},{"required":false,"hint":"Override pluralize() result in singular resource.","name":"plural","type":"string"},{"required":false,"hint":"Limits the list of RESTful routes to generate. Can include `show`, `new`, `create`, `edit`, `update`, and `delete`.","name":"only","type":"string"},{"required":false,"hint":"Excludes RESTful routes to generate, taking priority over the `only` argument. Can include `show`, `new`, `create`, `edit,` `update`, and `delete`.","name":"except","type":"string"},{"required":false,"hint":"Turn on shallow resources.","name":"shallow","type":"boolean"},{"required":false,"hint":"Shallow path prefix.","name":"shallowPath","type":"string"},{"required":false,"hint":"Shallow name prefix.","name":"shallowName","type":"string"},{"required":false,"hint":"Variable patterns to use for matching.","name":"constraints","type":"struct"},{"default":"resource","required":false,"name":"$call","type":"string"},{"default":false,"required":false,"name":"$plural","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Whether or not to add an optional `.[format]` pattern to the end of the generated routes. This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"resource","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // With default arguments\n    .resources("admins")\n\n    // Point authors URL to controller at `controllers/Users.cfc`\n    .resources(name="authors", controller="users")\n\n    // Limited list of routes generated by `only` argument.\n    .resources(name="products", only="index,show,edit,update")\n\n    // Limited list of routes generated by `except` argument.\n    .resources(name="orders", except="delete")\n\n    // Nested resources\n    .resources(name="stories", nested=true)\n      .resources("heroes")\n      .resources("villains")\n    .end()\n\n    // Overridden `path`\n    .resources(name="blogPostsOptions", path="blog-posts/options")\n.end();\n\n</cfscript>
"},"hint":"Create a group of routes that exposes actions for manipulating a collection of resources. A plural resource exposes URL patterns for the entire CRUD lifecycle (index, show, new, create, edit, update, delete), exposing a primary key in the URL for showing, editing, updating, and deleting records. If you need to generate routes for manipulating a singular resource without a primary key, see the resource mapper method.\n\n","returntype":"struct","slug":"mapper.resources","parameters":[{"required":true,"hint":"Camel-case name of resource to reference when build links and form actions. This is typically a plural word (e.g., `posts`).","name":"name","type":"string"},{"default":false,"required":false,"hint":"Whether or not additional calls will be nested within this resource.","name":"nested","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Override URL path representing this resource. Default is a dasherized version of `name` (e.g., `blogPosts` generates a path of `blog-posts`).","name":"path","type":"string"},{"required":false,"hint":"Override name of the controller used by resource. This defaults to the value provided for `name`.","name":"controller","type":"string"},{"required":false,"hint":"Override singularize() result in plural resources.","name":"singular","type":"string"},{"required":false,"hint":"Override pluralize() result in singular resource.","name":"plural","type":"string"},{"required":false,"hint":"Limits the list of RESTful routes to generate. Can include `index`, `show`, `new`, `create`, `edit`, `update`, and `delete`.","name":"only","type":"string"},{"required":false,"hint":"Excludes RESTful routes to generate, taking priority over the `only` argument. Can include `index`, `show`, `new`, `create`, `edit`, `update`, and `delete`.","name":"except","type":"string"},{"required":false,"hint":"Turn on shallow resources.","name":"shallow","type":"boolean"},{"required":false,"hint":"Shallow path prefix.","name":"shallowPath","type":"string"},{"required":false,"hint":"Shallow name prefix.","name":"shallowName","type":"string"},{"required":false,"hint":"Variable patterns to use for matching.","name":"constraints","type":"struct"},{"default":"[runtime expression]","required":false,"hint":"Whether or not to add an optional `.[format]` pattern to the end of the generated routes. This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"resources","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Render a view for the current action\nrenderView(action=\"show\");\n\n// Capture the response content\nwheelsResponse = response();\n\n// Log or inspect the response\nwriteDump(wheelsResponse);
"},"hint":"Returns the content that Wheels is preparing to send back to the client for the current request. This can include the output generated by renderView(), renderPartial(), renderText(), or any other rendering function that has been called during the request lifecycle. Essentially, response() lets you inspect or manipulate the final output before it is sent to the client, which can be particularly useful in testing, debugging, or middleware-style functions.\n\n","returntype":"string","slug":"controller.response","parameters":[],"availableIn":["controller"],"name":"response","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Application Home Page\nMap the root of the application (/) to a controller action:\n\n<cfscript>\nmapper()\n    .root(to="dashboards##show")\n.end();\n</cfscript>\n\n2. Root of a Namespaced Section (API)\nMap /api to an API controller:\n\n<cfscript>\nmapper()\n    .namespace("api")\n        .root(controller="apis", action="index")\n    .end();\n</cfscript>\n\n3. Root with Optional Format\nEnable clients to request JSON or XML directly:\n\n<cfscript>\nmapper()\n    .namespace("api")\n        .root(controller="apis", action="index", mapFormat=true)\n    .end();\n</cfscript>\n\n4. Root for Nested Resources\nUse root() inside a nested scope:\n\n<cfscript>\nmapper()\n    .namespace("admin")\n        .root(controller="dashboard", action="index")\n    .end();\n.end();\n</cfscript>\n
"},"hint":"Defines a route that matches the root of the current context. This could be the root of the entire application (like the home page) or the root of a namespaced section of your routes. It is commonly used to map a controller action to the main entry point of your application or a subsection of it. You can specify the controller and action either using the to argument (controller##action) or by passing controller and action separately. Optionally, mapFormat can be set to true to allow a format suffix like .json or .xml in the URL.\n\n","returntype":"struct","slug":"mapper.root","parameters":[{"required":false,"hint":"Set `controller##action` combination to map the route to. You may use either this argument or a combination of `controller` and `action`.","name":"to","type":"string"},{"required":false,"hint":"Set to `true` to include the format (e.g. `.json`) in the route.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"root","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic Save (Automatic INSERT/UPDATE)\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Alice";\nuser.lastName = "Smith";\nuser.email = "alice@example.com";\n\nif(user.save()){\n    writeOutput("User saved successfully!");\n} else {\n    writeOutput("Error saving user. Please check validations.");\n}\n</cfscript>\n\n2. Save Without Validations\n\n<cfscript>\nuser = model("user").findByKey(1);\nuser.firstName = ""; // Normally fails validation\n\n// Save without running validations\nuser.save(validate=false);\n</cfscript>\n\n3. Save Using Specific cfqueryparam Columns\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Bob";\nuser.lastName = "Jones";\nuser.email = "bob@example.com";\n\n// Only parameterize the `email` field\nuser.save(parameterize="email");\n</cfscript>\n\n4. Save Within a Transaction\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Charlie";\nuser.lastName = "Brown";\nuser.email = "charlie@example.com";\n\n// Attempt to save, but roll back instead of committing\nuser.save(transaction="rollback");\n</cfscript>\n\n5. Save and Handle Callbacks Manually\n\n<cfscript>\nuser = model("user").new();\nuser.firstName = "Dana";\nuser.lastName = "White";\nuser.email = "dana@example.com";\n\n// Save without triggering beforeSave/afterSave callbacks\nuser.save(callbacks=false);\n</cfscript>\n
"},"hint":"Saves the current model object to the database, with Wheels automatically determining whether to perform an INSERT for new objects or an UPDATE for existing ones. It returns true if the object was successfully saved, and false if the object failed validation or could not be saved. By default, save() also respects callbacks, validations, and parameterization, though these behaviors can be customized through optional arguments.\n\n","returntype":"boolean","slug":"model.save","parameters":[{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"}],"availableIn":["model"],"name":"save","tags":{"categoryClass":"crudfunctions","sectionClass":"modelclass","category":"CRUD Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Set a default controller for multiple routes\n\n<cfscript>\nmapper()\n    .scope(controller="freeForAll")\n        .get(name="bananas", action="bananas")\n        .root(action="index")\n    .end()\n.end();\n</cfscript>\n\n2. Apply a package/subfolder to multiple resources\n\n<cfscript>\nmapper()\n    .scope(package="public")\n        .resource(name="search", only="show,create")\n    .end()\n.end();\n</cfscript>\n\n3. Add a common URL path prefix\n\n<cfscript>\nmapper()\n    .scope(path="phones")\n        .get(name="newest", to="phones##newest")\n        .get(name="sortOfNew", to="phones##sortOfNew")\n    .end()\n.end();\n</cfscript>\n\n4. Combine controller and path scoping\n\n<cfscript>\nmapper()\n    .scope(controller="products", path="shop")\n        .get(name="featured", action="featured")\n        .get(name="sale", action="sale")\n    .end()\n.end();\n</cfscript>\n\n5. Use constraints for route variables\n\n<cfscript>\nmapper()\n    .scope(path="users", constraints={userId="\\d+"})\n        .get(name="profile", pattern="[userId]/profile", action="show")\n    .end()\n.end();\n</cfscript>\n
"},"hint":"The scope() function in Wheels is used to define a block of routes that share common parameters such as controller, package, path, or naming prefixes. All routes defined inside a scope() block automatically inherit these parameters unless explicitly overridden, making it easier to manage related routes. This is particularly useful for grouping routes under the same controller or package, adding a common URL prefix to multiple routes, applying shallow routing to nested resources, and reducing repetition while improving the maintainability of route definitions.\n\n","returntype":"struct","slug":"mapper.scope","parameters":[{"required":false,"hint":"Name to prepend to child route names for use when building links, forms, and other URLs.","name":"name","type":"string"},{"required":false,"hint":"Path to prefix to all child routes.","name":"path","type":"string"},{"required":false,"hint":"Package namespace to append to controllers.","name":"package","type":"string"},{"required":false,"hint":"Controller to use for routes.","name":"controller","type":"string"},{"required":false,"hint":"Turn on shallow resources to eliminate routing added before this one.","name":"shallow","type":"boolean"},{"required":false,"hint":"Shallow path prefix.","name":"shallowPath","type":"string"},{"required":false,"hint":"Shallow name prefix.","name":"shallowName","type":"string"},{"required":false,"hint":"Variable patterns to use for matching.","name":"constraints","type":"struct"},{"default":"scope","required":false,"name":"$call","type":"string"}],"availableIn":["mapper"],"name":"scope","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic select for seconds (0–59)\nsecondSelectTag(name="secondsToLaunch")\n\n2. Pre-select a second based on a parameter\nsecondSelectTag(name="secondsToLaunch", selected=params.secondsToLaunch)\n\n3. Only show 15-second intervals\nsecondSelectTag(name="secondsToLaunch", selected=params.secondsToLaunch, secondStep=15)\n\n4. Include a blank option with custom text\nsecondSelectTag(name="secondsToLaunch", includeBlank="- Select Seconds -")\n\n5. Add a label around the select control\nsecondSelectTag(name="secondsToLaunch", label="Launch Second", labelPlacement="around")
"},"hint":"Generates an HTML <select> form control populated with seconds (0–59) for a minute. You can bind it to a form parameter or manually set a selected value, control the step interval, include a blank option, and customize labels and HTML attributes. This is especially useful for time selection forms, like setting the seconds for a scheduled task or timestamp input.\n\n","returntype":"string","slug":"controller.secondSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The day that should be selected initially.","name":"selected","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"secondSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic select bound to a model property\nauthors = model("author").findAll();\n\n<!--- View code --->\n#select(objectName="book", property="authorId", options=authors)#\n\n2. Using valueField and textField\nselect(\n    objectName="book",\n    property="authorId",\n    options=authors,\n    valueField="id",\n    textField="authorfullname"\n)\n\n3. Include blank option\nselect(\n    objectName="book",\n    property="authorId",\n    options=authors,\n    valueField="id",\n    textField="authorfullname",\n    includeBlank="- Select Author -"\n)\n\n4. Nested hasMany association\n<cfloop from="1" to="#ArrayLen(shipments.orders)#" index="i">\n    select(\n        label="Order #shipments.orders[i].orderNum#",\n        objectName="shipment",\n        association="orders",\n        position=i,\n        property="statusId",\n        options=statuses,\n        valueField="id",\n        textField="name"\n    )\n</cfloop>\n\n5. Custom label and placement\nselect(\n    objectName="book",\n    property="authorId",\n    options=authors,\n    valueField="id",\n    textField="authorfullname",\n    label="Choose Author",\n    labelPlacement="before"\n)
"},"hint":"Builds and returns an HTML <select> element bound to a model object property. It automatically handles nested associations, labels, options, and error highlighting. You can provide a list of options as a query, array of objects, or simple array, and customize labels, HTML attributes, and encoding. It is especially useful for forms where a user must select a value from a predefined list that is related to a database model.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.select","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"required":false,"hint":"A collection to populate the select form control with. Can be a query recordset or an array of objects.","name":"options","type":"any"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `textField`","name":"valueField","type":"string"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element that the end user will see. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `valueField`","name":"textField","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"select","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic selectTag with a query\ncities = model("city").findAll();\n\n<!--- View code --->\n#selectTag(name="cityId", options=cities)#\n\n2. SelectTag with valueField and textField\nselectTag(\n    name="cityId",\n    options=cities,\n    valueField="id",\n    textField="name"\n)\n\n3. Including a blank option\nselectTag(\n    name="cityId",\n    options=cities,\n    valueField="id",\n    textField="name",\n    includeBlank="- Select a City -"\n)\n\n4. Multiple selection\nselectTag(\n    name="cityIds",\n    options=cities,\n    valueField="id",\n    textField="name",\n    multiple=true\n)\n\n5. Custom label and HTML wrapping\nselectTag(\n    name="cityId",\n    options=cities,\n    valueField="id",\n    textField="name",\n    label="Choose a City",\n    labelPlacement="before",\n    prepend="<div class='input-group'>",\n    append="</div>"\n)
"},"hint":"Builds an HTML <select> element using a name and a set of options. Unlike select(), it does not require a model object and is not bound to a property. It is useful for standalone select controls or when you want full manual control over the field.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.selectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"required":true,"hint":"A collection to populate the select form control with. Can be a query recordset or an array of objects.","name":"options","type":"any"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"Whether to allow multiple selection of options in the select form control.","name":"multiple","type":"boolean"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `textField`","name":"valueField","type":"string"},{"default":"","required":false,"hint":"The column or property to use for the value of each list element that the end user will see. Used only when a query or array of objects has been supplied in the options argument. Required when specifying `valueField`","name":"textField","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"selectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic email to a new user\nnewMember = model("member").findByKey(params.member.id);\n\nsendEmail(\n    to=newMember.email,\n    template="welcomeEmail",\n    subject="Thank You for Joining!",\n    recipientName=newMember.name,\n    startDate=newMember.startDate\n);\n\n2. Multipart email (HTML + text)\nsendEmail(\n    to="user@example.com",\n    template="welcomeEmailText, welcomeEmailHTML",\n    subject="Welcome!",\n    detectMultipart=true\n);\n\n3. Email with a layout\nsendEmail(\n    to="user@example.com",\n    template="newsletter",\n    layout="emailLayout",\n    subject="Monthly Newsletter",\n    userName="Salman"\n);\n\n4. Email with attachments\nsendEmail(\n    to="user@example.com",\n    template="reportEmail",\n    subject="Your Monthly Report",\n    file="report.pdf, summary.xlsx"\n);\n\n5. Write email to a file without sending\nsendEmail(\n    to="user@example.com",\n    template="testEmail",\n    subject="Testing Email",\n    writeToFile="#expandPath('./tmp/testEmail.eml')#",\n    deliver=false\n);
"},"hint":"Sends an email using a template and an optional layout to wrap it in.\nBesides the Wheels-specific arguments documented here, you can also pass in any argument that is accepted by the cfmail tag as well as your own arguments to be used by the view.\n\n","returntype":"any","slug":"controller.sendEmail","parameters":[{"default":"","required":true,"hint":"The path to the email template or two paths if you want to send a multipart email. if the `detectMultipart` argument is `false`, the template for the text version should be the first one in the list. This argument is also aliased as `templates`.","name":"template","type":"string"},{"default":"","required":true,"hint":"Email address to send from.","name":"from","type":"string"},{"default":"","required":true,"hint":"List of email addresses to send the email to.","name":"to","type":"string"},{"default":"","required":true,"hint":"The subject line of the email.","name":"subject","type":"string"},{"default":false,"required":false,"hint":"Layout(s) to wrap the email template in. This argument is also aliased as `layouts`.","name":"layout","type":"any"},{"default":"","required":false,"hint":"A list of the names of the files to attach to the email. This will reference files stored in the `files` folder (or a path relative to it). This argument is also aliased as `files`.","name":"file","type":"string"},{"default":true,"required":false,"hint":"When set to `true` and multiple values are provided for the `template` argument, Wheels will detect which of the templates is text and which one is HTML (by counting the `<` characters).","name":"detectMultipart","type":"boolean"},{"default":true,"required":false,"hint":"When set to `false`, the email will not be sent.","name":"deliver","type":"boolean"},{"default":"","required":false,"hint":"The file to which the email contents will be written","name":"writeToFile","type":"string"}],"availableIn":["controller"],"name":"sendEmail","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Send a file for download from the files folder\nsendFile(file="wheels_tutorial_20081028_J657D6HX.pdf");\n\n2. Rename the file for the client\nsendFile(\n    file="wheels_tutorial_20081028_J657D6HX.pdf",\n    name="Tutorial.pdf"\n);\n\n3. Send a file located outside the web root\nsendFile(\n    file="../../tutorials/wheels_tutorial_20081028_J657D6HX.pdf"\n);\n\n4. Inline display instead of download\nsendFile(\n    file="brochure.pdf",\n    disposition="inline",\n    type="application/pdf"\n);\n\n5. Delete file after sending\nsendFile(\n    file="temporary_report.xlsx",\n    deleteFile=true\n);
"},"hint":"Sends a file to the client. By default, it serves files from the public/files folder in your project or a path relative to it. You can control how the file is presented to the user (download dialog vs inline display), set the content type, rename it for the client, or even delete it from the server after delivery.\n\n","returntype":"any","slug":"controller.sendFile","parameters":[{"required":true,"hint":"The file to send to the user.","name":"file","type":"string"},{"default":"","required":false,"hint":"The file name to show in the browser download dialog box.","name":"name","type":"string"},{"default":"","required":false,"hint":"The HTTP content type to deliver the file as.","name":"type","type":"string"},{"default":"attachment","required":false,"hint":"Set to `inline` to have the browser handle the opening of the file (possibly inline in the browser) or set to `attachment` to force a download dialog box.","name":"disposition","type":"string"},{"default":"","required":false,"hint":"Directory outside of the web root where the file exists. Must be a full path.","name":"directory","type":"string"},{"default":false,"required":false,"hint":"Pass in `true` to delete the file on the server after sending it.","name":"deleteFile","type":"boolean"},{"default":true,"required":false,"name":"deliver","type":"boolean"}],"availableIn":["controller"],"name":"sendFile","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"controller","category":"Miscellaneous Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Set the `URLRewriting` setting to `Partial`.\nset(URLRewriting="Partial");\n\n2. Set default values for the arguments in the `buttonTo` view helper. This works for the majority of Wheels functions/arguments.\nset(functionName="buttonTo", onlyPath=true, host="", protocol="", port=0, text="", confirm="", image="", disable="");\n\n3. Set the default values for a form helper to get the form marked up to your preferences.\nset(functionName="textField", labelPlacement="before", prependToLabel="<div>", append="</div>", appendToLabel="<br>"):\n
"},"hint":"Used to configure global settings or set default argument values for Wheels functions. It can be applied to core functions, helpers, and even migrations. This allows you to define a standard behavior across your application without repeating arguments every time a function is called.\n\n","returntype":"void","slug":"controller.set","parameters":[],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"set","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"configuration","category":"Miscellaneous Functions","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic filter chain\n// Set filter chain directly\nsetFilterChain([\n    {through="restrictAccess"}, // runs for all actions by default\n    {through="isLoggedIn, checkIPAddress", except="home, login"}, // exclude certain actions\n    {type="after", through="logConversion", only="thankYou"} // after filter for specific action\n]);\n\n//First filter: restrictAccess runs before all actions.\n//Second filter: isLoggedIn and checkIPAddress run before all actions except home and login.\n//Third filter: logConversion runs after the thankYou action only.\n\n2. Using only and except with different filter types\nsetFilterChain([\n    {through="authenticateUser", only="edit, update, delete"}, // only for sensitive actions\n    {through="trackActivity", except="index, show"},           // for most actions except viewing\n    {type="after", through="sendAnalytics"}                   // after all actions\n]);\n\n//Demonstrates selective filtering with only and except.\n//Can combine before (default) and after filters in the same chain.\n\n3. Multiple filters in one chain struct\nsetFilterChain([\n    {through="validateSession, checkPermissions", only="admin, settings"},\n    {through="logRequest"},\n    {type="after", through="cleanupTempFiles"}\n]);\n\n//Multiple filters can run together (validateSession and checkPermissions).\n//Mix of before and after filters ensures proper order and execution context.
"},"hint":"Provides a low-level way to define the complete filter chain for a controller. This lets you explicitly specify the sequence of filters, their scope, and the actions they apply to, all in a single configuration. Filters are functions that run before, after, or around actions to handle tasks such as authentication, logging, or IP restrictions.\n\n","returntype":"void","slug":"controller.setFilterChain","parameters":[{"required":true,"hint":"An array of structs, each of which represent an `argumentCollection` that get passed to the `filters` function. This should represent the entire filter chain that you want to use for this controller.","name":"chain","type":"array"}],"availableIn":["controller"],"name":"setFilterChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Set the flash to cookie for the current controller only.\nsetFlashStorage("cookie");\n\n2. Set the flash to session for the current controller and application\nsetFlashStorage("session", true);
"},"hint":"Dynamically sets the storage mechanism for flash messages during the current request lifecycle. Flash messages are temporary messages (e.g., success or error notifications) that persist across requests.\n\n","returntype":"void","slug":"controller.setFlashStorage","parameters":[{"default":"session","required":false,"hint":"Specifies the storage mechanism for flash data. Available options: session or cookie.","name":"storage","type":"string"},{"default":false,"required":false,"hint":"If set to true, updates both application-level and controller-level flashStorage; otherwise, only the controller-level flashStorage is updated.","name":"setGlobally","type":"boolean"}],"availableIn":["controller"],"name":"setFlashStorage","tags":{"categoryClass":"flashfunctions","sectionClass":"controller","category":"Flash Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
/* Note that there are two ways to do pagination yourself using a custom query.\n\t1) Do a query that grabs everything that matches and then use\n\tthe `cfouput` or `cfloop` tag to page through the results.\n\t2) Use your database to make 2 queries. The first query\n\tbasically does a count of the total number of records that match\n\tthe criteria and the second query actually selects the page of\n\trecords for retrieval.\n\tIn the example below, we will show how to write a custom query\n\tusing both of these methods. Note that the syntax where your\n\tdatabase performs the pagination will differ depending on the\n\tdatabase engine you are using. Plese consult your database\n\tengine's documentation for the correct syntax.\n\tAlso note that the view code will differ depending on the method\n\tused.\n*/\n\n//=================== First method: Handle the pagination through your CFML engine\n\n// Model code: In your model (ie. User.cfc), create a custom method for your custom query\nfunction myCustomQuery(required numeric page, numeric perPage=25){\n\tlocal.customQuery=QueryExecute("SELECT * FROM users", [], { datasource=get('dataSourceName') });\n\tsetPagination(\n\t\ttotalRecords=local.customQuery.RecordCount,\n\t\tcurrentPage=arguments.page,\n\t\tperPage=arguments.perPage,\n\t\thandle="myCustomQueryHandle");\n\treturn local.customQuery;\n}\n\n// Controller code\nfunction list(){\n\tparam name="params.page" default="1;\n\tparam name="params.perPage" default="25";\n\tallUsers = model("user").myCustomQuery( page=params.page, perPage=params.perPage);\n\n\t// Because we're going to let `cfoutput`/`cfloop` handle the pagination,\n\t// we're going to need to get some addition information about the pagination.\n\tpaginationData = pagination("myCustomQueryHandle")\n}\n\n<!--- View code (using `cfloop`): Use the information from `paginationData` to page through the records --->\n<ul>\n\t<cfloop query="allUsers" startrow="#paginationData.startrow#" endrow="#paginationData.endrow#" >\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfloop>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n<!--- View code (using `cfoutput`) Use the information from `paginationData` to page through the records--->\n<ul>\n\t<cfoutput query="allUsers" startrow="#paginationData.startrow#" maxrows="#paginationData.maxrows#" >\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfoutput>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n//=================== Second method: Handle the pagination through the database\n\n// Model code: In your model (ie. `User.cfc`), create a custom method for your custom query\n\nfunction myCustomQuery(required numeric page, numeric perPage=25){\n\tlocal.customQueryCount=QueryExecute("SELECT COUNT(*) AS theCount FROM users",\n\t\t\t\t\t\t\t\t\t\t[], { datasource=get('dataSourceName') });\n\tlocal.customQuery=QueryExecute("SELECT * FROM users LIMIT ? OFFSET ?",\n\t\t\t\t\t\t\t\t\t[arguments.page, arguments.perPage],\n\t\t\t\t\t\t\t\t\t{ datasource=get('dataSourceName') });\n\n\t//Notice the we use the value from the first query for `totalRecords`\n\tsetPagination(\n\t\ttotalRecords=local.customQueryCount.theCount,\n\t\tcurrentPage=arguments.page,\n\t\tperPage=arguments.perPage,\n\t\thandle="myCustomQueryHandle" );\n\n\t// We return the second query\n\treturn local.customQuery;\n}\n\n// Controller code\nfunction list(){\n\tparam name="params.page" default="1;\n\tparam name="params.perPage" default="25";\n\tallUsers = model("user").myCustomQuery( page=params.page, perPage=params.perPage);\n}\n\n<!--- View code (using `cfloop`)--->\n<ul>\n\t<cfloop query="allUsers">\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfloop>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n<!--- View code (using `cfoutput`)--->\n<ul>\n\t<cfoutput query="allUsers">\n\t<li> #allUsers.firstName# #allUsers.lastName# </li>\n\t</cfoutput>\n</ul>\n#paginationLinks(handle="myCustomQueryHandle")#\n\n
"},"hint":"Aallows you to define a pagination handle for a custom query so that you can easily generate paginated links and manage page offsets in your views. It’s useful when you want manual or database-driven pagination instead of relying on built-in model queries. This works in combination with the pagination() function to retrieve pagination metadata (like startRow, endRow, maxRows) and paginationLinks() to render links in your view.\n\n","returntype":"void","slug":"controller.setPagination","parameters":[{"required":true,"hint":"Total count of records that should be represented by the paginated links.","name":"totalRecords","type":"numeric"},{"default":1,"required":false,"hint":"Page number that should be represented by the data being fetched and the paginated links.","name":"currentPage","type":"numeric"},{"default":25,"required":false,"hint":"Number of records that should be represented on each page of data.","name":"perPage","type":"numeric"},{"default":"query","required":false,"hint":"Name of handle to reference in `paginationLinks`.","name":"handle","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"setPagination","tags":{"categoryClass":"paginationfunctions","sectionClass":"controller","category":"Pagination Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Single primary key\ncomponent extends="Model" {\n    function config() {\n        // The primary key for this table is `userID`\n        setPrimaryKey("userID");\n    }\n}\n\n2. Composite primary key\ncomponent extends="Model" {\n    function config() {\n        // The combination of `orderID` and `productID` uniquely identifies a record\n        setPrimaryKey("orderID,productID");\n    }\n}\n\n3. Using the alias setPrimaryKeys()\ncomponent extends="Model" {\n    function config() {\n        // Alias works the same as `setPrimaryKey()`\n        setPrimaryKeys("customerID");\n    }\n}
"},"hint":"The setPrimaryKey() function allows you to define which property (or properties) of a model represent the primary key in the database. This is crucial for Wheels to correctly handle CRUD operations, updates, and record lookups. For single-column primary keys, pass the property name as a string. For composite primary keys (multiple columns together form the key), pass a comma-separated list of property names. Alias: setPrimaryKeys()\n\n","returntype":"void","slug":"model.setPrimaryKey","parameters":[{"required":true,"hint":"Property (or list of properties) to set as the primary key.","name":"property","type":"string"}],"availableIn":["model"],"name":"setPrimaryKey","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// In `models/Subscription.cfc`, define the primary key as composite of the columns `customerId` and `publicationId`.\nfunction config(){\n\tsetPrimaryKeys("customerId,publicationId");\n}
"},"hint":"Alias for setPrimaryKey().\nUse this for better readability when you're setting multiple properties as the primary key.\n\n","returntype":"void","slug":"model.setPrimaryKeys","parameters":[{"required":true,"hint":"Property (or list of properties) to set as the primary key.","name":"property","type":"string"}],"availableIn":["model"],"name":"setPrimaryKeys","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Using a struct (common scenario with form submission)\n// Controller code: create new user\nuser = model("user").new();\n\n// Set properties from a submitted form\nuser.setProperties(params.user);\n\n// Save the updated user\nuser.save();\n\n2. Using named arguments\nuser = model("user").new();\n\n// Set properties directly using named arguments\nuser.setProperties(\n    firstName="John",\n    lastName="Doe",\n    email="john.doe@example.com"\n);\n\n// Save changes\nuser.save();\n\n3. Using with validations\nuser = model("user").new();\n\n// Set multiple properties, skipping one intentionally\nuser.setProperties({\n    firstName = "Jane",\n    lastName = "Smith"\n});\n\n// Only save if validations pass\nif(user.save()){\n    writeOutput("User updated successfully!");\n} else {\n    writeDump(user.errors);\n}
"},"hint":"Allows you to set multiple properties of a model object at once. It is useful when you want to update a model with a structure (struct) of key/value pairs instead of assigning each property individually. The keys of the struct should match the property names of the model. You can also pass named arguments directly instead of a struct.\n\n","returntype":"void","slug":"model.setProperties","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"}],"availableIn":["model"],"name":"setProperties","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelobject","category":"Miscellaneous Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Sending plain text\nfunction myAction() {\n    setResponse("This is a custom response sent directly to the client.");\n}\n\n2. Sending JSON content\nfunction getUserData() {\n    user = model("user").findByKey(1);\n    \n    // Convert the user object to JSON\n    jsonData = serializeJson(user);\n    \n    // Set the JSON response\n    setResponse(jsonData);\n}\ncfheader(name="Content-Type", value="application/json");\n\n3. Sending HTML content\nfunction showCustomHtml() {\n    htmlContent = "<h1>Welcome!</h1><p>This is a custom HTML response.</p>";\n    setResponse(htmlContent);\n}
"},"hint":"Allows you to manually set the content that Wheels will send back to the client for a given request. Unlike renderView() or renderText(), which automatically generate output from templates or data, setResponse() gives you full control over the response content.\n\n","returntype":"void","slug":"controller.setResponse","parameters":[{"required":true,"hint":"The content to send to the client.","name":"content","type":"string"}],"availableIn":["controller"],"name":"setResponse","tags":{"categoryClass":"renderingfunctions","sectionClass":"controller","category":"Rendering Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Basic prefix\n// In app/models/User.cfc\nfunction config(){\n    // All queries will now target 'tblUsers' instead of 'users'\n    setTableNamePrefix("tbl");\n}\n\n2. Using a custom prefix for multiple models\n// app/models/Product.cfc\nfunction config(){\n    setTableNamePrefix("tbl");\n}\n\n// app/models/Order.cfc\nfunction config(){\n    setTableNamePrefix("tbl");\n}
"},"hint":"Allows you to add a prefix to the table name used by a model when performing SQL queries. This is useful if your database uses a consistent naming convention, such as tblUsers instead of Users. By default, Wheels infers the table name from the model name (e.g., User -> users). Using a prefix ensures that all queries automatically reference the correctly prefixed table.\n\n","returntype":"void","slug":"model.setTableNamePrefix","parameters":[{"required":true,"hint":"A prefix to prepend to the table name.","name":"prefix","type":"string"}],"availableIn":["model"],"name":"setTableNamePrefix","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic setup for a test suite\ncomponent extends="app.tests.Test" {\n\n    function setup() {\n        // Initialize a new user object before each test\n        variables.user = model("user").new();\n    }\n\n    function test_User_Creation() {\n        variables.user.firstName = "John";\n        variables.user.lastName = "Doe";\n\n        assert("variables.user.save() eq true");\n    }\n\n    function test_User_Email_Validation() {\n        variables.user.email = "invalid-email";\n\n        assert("variables.user.valid() eq false");\n    }\n}\n\n2. Reset database table before each test\ncomponent extends="app.tests.Test" {\n\n    function setup() {\n        // Delete all records in the users table before each test\n        model("user").deleteAll();\n    }\n\n    function test_User_Insert() {\n        newUser = model("user").new(firstName="Alice", lastName="Smith");\n        assert("newUser.save() eq true");\n    }\n\n    function test_User_Count() {\n        count = model("user").count();\n        assert("count eq 0");\n    }\n}
"},"hint":"Callback used in Wheels legacy testing framework. It runs before every individual test case within a test suite. This allows you to prepare the test environment, initialize objects, or reset state before each test executes.\n\n","returntype":"any","slug":"test.setup","parameters":[],"availableIn":["test"],"name":"setup","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic verification chain\ncomponent extends="Controller" {\n\n    function init() {\n        // Set verification rules for multiple actions\n        setVerificationChain([\n            {only="handleForm", post=true},\n            {only="edit", get=true, params="userId", paramsTypes="integer"}\n        ]);\n    }\n\n    function handleForm() {\n        // Action logic here\n    }\n\n    function edit() {\n        // Action logic here\n    }\n}\n\n2. Adding custom error handling\ncomponent extends="Controller" {\n\n    function init() {\n        setVerificationChain([\n            {only="edit", get=true, params="userId", paramsTypes="integer", handler="index", error="Invalid userId"},\n            {only="delete", post=true, params="id", paramsTypes="integer", error="Missing or invalid id"}\n        ]);\n    }\n\n    function edit() {\n        /* edit logic */ \n    }\n    function delete() {\n        /* delete logic */ \n    }\n}
"},"hint":"Allows you to define the entire verification chain for a controller in a low-level, structured way. Verification chains are used to validate requests, ensuring they meet specific requirements (like HTTP method, parameters, or types) before the controller action executes. Instead of defining individual verifies() calls in each action, you can use setVerificationChain() to set all verifications at once.\n\n","returntype":"void","slug":"controller.setVerificationChain","parameters":[{"required":true,"hint":"An array of structs, each of which represent an `argumentCollection` that get passed to the `verifies` function. This should represent the entire verification chain that you want to use for this controller.","name":"chain","type":"array"}],"availableIn":["controller"],"name":"setVerificationChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Typical usage\n#simpleFormat(post.bodyText)#\n\nIf post.bodyText =\nThis is the first line.\n\nThis is the second paragraph.\n\nOutput:\n\n<p>This is the first line.</p>\n<p>This is the second paragraph.</p>\n\n2. Demonstrating line breaks\n<cfsavecontent variable="comment">\nI love this post!\n\nHere's why:\n* Short\n* Succinct\n* Awesome\n</cfsavecontent>\n\n#simpleFormat(comment)#\n\nOutput:\n\n<p>I love this post!</p>\n<p>Here's why:<br>\n* Short<br>\n* Succinct<br>\n* Awesome</p>\n\n3. Disable paragraph wrapping\n<cfsavecontent variable="bio">\nHello, I’m Salman.\nI write about ColdFusion and backend development.\n</cfsavecontent>\n\n#simpleFormat(bio, wrap=false)#\n\nOutput:\n\nHello, I’m Salman.<br>\nI write about ColdFusion and backend development.\n\n//No <p> tags, only <br> for newlines.\n\n4. Handling user input safely\nWhen you’re rendering user-submitted text in HTML attributes, simpleFormat() alone is not enough:\n\n<!-- Incorrect usage in an attribute -->\n<div title="#simpleFormat(userInput)#">...</div>\n\nInstead, combine with EncodeForHtmlAttribute():\n\n<div title="#EncodeForHtmlAttribute(simpleFormat(userInput))#">...</div>
"},"hint":"Takes plain text and converts newline and carriage return characters into HTML <br> and <p> tags for display in a browser. This is particularly useful for rendering user-submitted text (like blog posts, comments, or descriptions) in a way that respects the author’s formatting. By default, the text is wrapped in a <p> element and URL parameters are encoded for safety.\n\n","returntype":"string","slug":"controller.simpleFormat","parameters":[{"required":true,"hint":"The text to format.","name":"text","type":"string"},{"default":true,"required":false,"hint":"Set to `true` to wrap the result in a paragraph HTML element.","name":"wrap","type":"boolean"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"simpleFormat","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"viewhelpers","category":"Miscellaneous Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Simple plural -> singular\n#singularize("languages")#\n\nOutput:\nlanguage\n\n2. Words ending in -ies\n#singularize("companies")#\n\nOutput:\ncompany\n\n3. Words ending in -es\n#singularize("boxes")#\n\nOutput:\nbox\n\n4. Irregular plural\n#singularize("children")#\n\nOutput:\nchild
"},"hint":"Converts a plural word into its singular form. It uses Wheels’ built-in inflection rules, handling common English pluralization cases as well as irregular words. This is useful when dynamically generating model names, table names, or working with resource naming conventions.\n\n","returntype":"string","slug":"controller.singularize","parameters":[{"required":true,"name":"word","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"singularize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic form for create action\n#startFormTag(action="create")#\n    #textFieldTag(name="firstName")#\n    #submitTag(value="Save")#\n#endFormTag()#\n\n2. Form with file upload\n#startFormTag(action="upload", multipart=true)#\n    #fileFieldTag(name="profilePicture")#\n    #submitTag(value="Upload")#\n#endFormTag()#\n\n3. Using a named route\n#startFormTag(route="registerUser")#\n    #textFieldTag(name="email")#\n    #passwordFieldTag(name="password")#\n    #submitTag(value="Register")#\n#endFormTag()#\n\n4. Passing keys and params\n#startFormTag(controller="posts", action="edit", key=42, params="draft=true")#\n    #textAreaTag(name="content")#\n    #submitTag(value="Update Post")#\n#endFormTag()#\n\n5. Custom attributes\n#startFormTag(action="search", id="searchForm", class="inline-form")#\n    #textFieldTag(name="q")#\n    #submitTag(value="Search")#\n#endFormTag()#
"},"hint":"Builds and returns an opening <form> tag. The form’s action URL is automatically generated following the same rules as urlFor(). You can pass standard Wheels routing arguments (controller, action, route, key, params) as well as custom HTML attributes (id, class, rel, etc.). Use this in combination with endFormTag() to wrap your form controls.\n\n","returntype":"string","slug":"controller.startFormTag","parameters":[{"default":"post","required":false,"hint":"The type of `method` to use in the `form` tag (`delete`, `get`, `patch`, `post`, and `put` are the options).","name":"method","type":"string"},{"default":false,"required":false,"hint":"Set to `true` if the form should be able to upload files.","name":"multipart","type":"boolean"},{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: wheels=cool&x=y). Please note that Wheels uses the & and = characters to split the parameters and encode them properly for you. However, if you need to pass in & or = as part of the value, then you need to encode them (and only them), example: a=cats%26dogs%3Dtrouble!&b=1.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If true, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"startFormTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple string column\nt.string("username");\n\n2. Limit the length of the string\nt.string(columnNames="email", limit=255);\n\n3. Set default values\nt.string(columnNames="status", default="active");\n\n4. Multiple columns in one call\nt.string(columnNames="firstName,lastName");\n\n5. Nullable vs non-nullable\nt.string(columnNames="configKey", allowNull=false);\nt.string(columnNames="configValue", allowNull=true);
"},"hint":"Used to add one or more string (VARCHAR) columns to a database table. It supports specifying default values, nullability, and a maximum length (limit). Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.string","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"limit","type":"any"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"string","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Remove links but keep text\n#stripLinks('<strong>Wheels</strong> is a framework for <a href="http://www.adobe.com/products/coldfusion">ColdFusion</a>.')#\n\nOutput:\n<strong>Wheels</strong> is a framework for ColdFusion.\n\n2. Strip links from user-submitted content\nuserComment = '<p>Check out <a href="http://spam.com">this link</a>!</p>';\n#stripLinks(userComment)#\n\nOutput:\n<p>Check out this link!</p>\n\n3. Encoding URLs (optional)\n#stripLinks('<a href="http://example.com/page?param=value&another=1">Example</a>', encode=false)#\n\nOutput:\nExample
"},"hint":"Removes all <a> tags (hyperlinks) from an HTML string while preserving the inner text. This is useful when you want to display content without clickable links but still retain the text inside them.\n\n","returntype":"string","slug":"controller.stripLinks","parameters":[{"required":true,"hint":"The HTML to remove links from.","name":"html","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"stripLinks","tags":{"categoryClass":"sanitizationfunctions","sectionClass":"viewhelpers","category":"Sanitization Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Remove all tags from a string\n#stripTags('<strong>Wheels</strong> is a framework for <a href="http://www.adobe.com/products/coldfusion">ColdFusion</a>.')#\n\nOutput:\nWheels is a framework for ColdFusion.\n\n2. Sanitize user input\nuserInput = '<script>alert("xss")</script>Normal text';\n#stripTags(userInput)#\n\nOutput:\nNormal text\n\n3. With encoding\n#stripTags('<a href="http://example.com/page?param=value&another=1">Example</a>')#\n\nOutput:\nExample
"},"hint":"Removes all HTML tags from a string, leaving only the raw text content. Use this when you need to sanitize HTML by completely removing formatting and markup.\n\n","returntype":"string","slug":"controller.stripTags","parameters":[{"required":true,"hint":"The HTML to remove tag markup from.","name":"html","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"stripTags","tags":{"categoryClass":"sanitizationfunctions","sectionClass":"viewhelpers","category":"Sanitization Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
<!--- view code --->\n<head>\n    <!--- Includes `public/stylesheets/styles.css` --->\n    #styleSheetLinkTag("styles")#\n    <!--- Includes `public/stylesheets/blog.css` and `public/stylesheets/comments.css` --->\n    #styleSheetLinkTag("blog,comments")#\n    <!--- Includes printer style sheet --->\n    #styleSheetLinkTag(sources="print", media="print")#\n    <!--- Includes external style sheet --->\n    #styleSheetLinkTag("http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/cupertino/jquery-ui.css")#\n</head>\n\n<body>\n    <!--- This will still appear in the `head` --->\n    #styleSheetLinkTag(sources="tabs", head=true)#\n</body>
"},"hint":"Generates one or more <link> tags for including CSS stylesheets in your application. By default, it looks in the publicstylesheets folder of your app but can also handle external URLs or place stylesheets directly in the <head> section when needed.\n\n","returntype":"string","slug":"controller.styleSheetLinkTag","parameters":[{"default":"","required":false,"hint":"The name of one or many CSS files in the stylesheets folder, minus the `.css` extension. Pass a full URL to generate a tag for an external style sheet. Can also be called with the `source` argument.","name":"sources","type":"string"},{"default":"text/css","required":false,"hint":"The `type` attribute for the `link` tag.","name":"type","type":"string"},{"default":"all","required":false,"hint":"The `media` attribute for the `link` tag.","name":"media","type":"string"},{"required":false,"hint":"The `rel` attribute for the relation between the tag and href.","name":"rel","type":"string"},{"default":false,"required":false,"hint":"Set to `true` to place the output in the `head` area of the HTML page instead of the default behavior (which is to place the output where the function is called from).","name":"head","type":"boolean"},{"default":",","required":false,"hint":"The delimiter to use for the list of CSS files.","name":"delim","type":"string"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"}],"availableIn":["controller"],"name":"styleSheetLinkTag","tags":{"categoryClass":"assetfunctions","sectionClass":"viewhelpers","category":"Asset Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Default submit button\n#startFormTag(action="save")##submitTag()##endFormTag()#\n2. Custom button label\n#submitTag(value="Register Now")#\n3. Submit button with CSS class and ID\n#submitTag(value="Update Profile", class="btn btn-primary", id="updateBtn")#\n4. Submit as an image button\n#submitTag(image="submit-icon.png", value="Submit Form")#\n5. Wrapping with prepend and append\n#submitTag(value="Send Message", prepend="<div class='form-actions'>", append="</div>")#
"},"hint":"Builds and returns a string containing a submit button form control.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.submitTag","parameters":[{"default":"Save changes","required":false,"hint":"Message to display in the button form control.","name":"value","type":"string"},{"default":"","required":false,"hint":"File name of the image file to use in the button form control.","name":"image","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"submitTag","tags":{"categoryClass":"generalformfunctions","sectionClass":"viewhelpers","category":"General Form Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic sum\nallSalaries = model("employee").sum("salary");\n\n2. With filtering (where)\nallAustralianSalaries = model("employee").sum(\n property="salary",\n include="country",\n where="countryname='Australia'"\n);\n\n3. With ifNull safeguard\nsalarySum = model("employee").sum(\n property="salary",\n where="salary BETWEEN #params.min# AND #params.max#",\n ifNull=0\n);\n\n4. Sum with grouping\nsalariesByDept = model("employee").sum(\n property="salary",\n group="departmentId"\n);\n\n5. Distinct sum\nuniqueSalaries = model("employee").sum(\n property="salary",\n distinct=true\n);\n
"},"hint":"Calculates the total of all values for a given property (column) using SQL’s SUM() function. It’s typically used to aggregate numeric values across a set of records (e.g., summing salaries, prices, or quantities). You can add filtering with where, group results with group, or join associations using include. If no records are found, use the ifNull argument to return a safe default (commonly 0 for numeric sums).\n\n","returntype":"any","slug":"model.sum","parameters":[{"required":true,"hint":"Name of the property to get the sum for (must be a property of a numeric data type).","name":"property","type":"string"},{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":false,"required":false,"hint":"When true, SUM returns the sum of unique values only.","name":"distinct","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"","required":false,"hint":"The value returned if no records are found. Common usage is to set this to `0` to make sure a numeric value is always returned instead of a blank string.","name":"ifNull","type":"any"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"},{"required":false,"hint":"Maps to the `GROUP BY` clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"group","type":"string"}],"availableIn":["model"],"name":"sum","tags":{"categoryClass":"statisticsfunctions","sectionClass":"modelclass","category":"Statistics Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic override for custom table name\n// In app/models/User.cfc\nfunction config() {\n // Tell Wheels to use the `tbl_USERS` table instead of the default `users`.\n table("tbl_USERS");\n}\n\n2. Using a table with a completely different name\n// In app/models/Order.cfc\nfunction config() {\n // Map the Order model to a table named `sales_transactions`.\n table("sales_transactions");\n}\n\n3. Disabling table mapping for a non-database model\n// In app/models/Notification.cfc\nfunction config() {\n // This model will not connect to any table.\n table(false);\n}\n\n4. Working with legacy naming conventions\n// In app/models/Product.cfc\nfunction config() {\n // The database uses uppercase with prefixes for tables.\n table("LEGACY_PRODUCTS_TABLE");\n}\n
"},"hint":"Used to tell Wheels which database table a model should connect to. Normally, Wheels automatically maps a model name to a plural table name (for example, a model named User maps to the users table). However, when your database uses custom naming conventions that do not match the Wheels defaults, you can override the mapping by explicitly specifying the table name with table(). If you want a model to not be tied to any database table at all, you can set table(false). This is useful for models that are used purely for logic, service layers, or scenarios where the model acts as a data wrapper without persistence.\n\n","returntype":"void","slug":"model.table","parameters":[{"required":true,"hint":"Name of the table to map this model to.","name":"name","type":"any"}],"availableIn":["model"],"name":"table","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelconfiguration","category":"Miscellaneous Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Check what table the user model uses\nwhatAmIMappedTo = model("user").tableName();
"},"hint":"Returns the name of the database table that a model is mapped to. Wheels automatically determines the table name based on its naming convention, where a singular model name maps to a plural table name (for example, a model named User maps to the users table). If the table has been explicitly overridden using the table() function in the model’s config(), then tableName() will return the custom mapping instead. This function is useful when you want to programmatically check or log the database table a model is connected to, especially in projects with mixed or legacy naming conventions.\n\n","returntype":"string","slug":"model.tableName","parameters":[],"availableIn":["model"],"name":"tableName","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Basic cleanup after tests\nfunction teardown() {\n // Remove temporary data created during the test\n queryExecute("DELETE FROM users WHERE email LIKE 'testuser%@example.com'");\n}\n\n2. Resetting application variables\nfunction teardown() {\n // Clear session values to avoid leaking between tests\n structClear(session);\n}\n\n3. Rolling back test data with transactions\nfunction teardown() {\n // Roll back the transaction started in setup\n transaction action="rollback";\n}\n\n4. Cleaning up mock objects or stubs\nfunction teardown() {\n // Reset mock services after each test\n variables.mockService.reset();\n}\n
"},"hint":"Callback that executes after every test case when using Wheels’ legacy testing framework. It is typically used to clean up any data, variables, or state changes made during a test, ensuring that each test runs in isolation and does not interfere with subsequent tests. This helps maintain reliability and consistency across the test suite.\n\n","returntype":"any","slug":"test.teardown","parameters":[],"availableIn":["test"],"name":"teardown","tags":{"categoryClass":"callbackfunctions","sectionClass":"testmodelconfiguration","category":"Callback Functions","section":"Test Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. In a migration file\nt.text("description");\n\n2. Creates both summary and notes columns as text types\nt.text("summary,notes");\n\n3. Adds a column with a default placeholder text\nt.text(columnNames="details", default="N/A");\n\n4. Adds a text column that must always have a value\nt.text(columnNames="bio", allowNull=false);\n\n5. Adds a column with a default value and disallows nulls\nt.text(columnNames="comments", default="No comments provided", allowNull=false);\n
"},"hint":"Used within a migration to add one or more text columns to a database table definition. Text columns are designed for storing larger amounts of character data compared to standard string or varchar columns. This function allows you to define the column name, set a default value, and control whether the column allows null values.\n\n","returntype":"any","slug":"tabledefinition.text","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"text","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Basic textarea with label\n#textArea(label="Overview", objectName="article", property="overview")#\n\n2. Customizing with HTML attributes\n#textArea(\n label="Comments", \n objectName="post", \n property="comments", \n class="form-control", \n id="commentsBox", \n rows="5", \n cols="50"\n)#\n\n3. Using with nested associations\n<fieldset>\n <legend>Screenshots</legend>\n <cfloop from="1" to="#ArrayLen(site.screenshots)#" index="i">\n #fileField(label="File #i#", objectName="site", association="screenshots", position=i, property="file")#\n #textArea(label="Caption #i#", objectName="site", association="screenshots", position=i, property="caption")#\n </cfloop>\n</fieldset>\n\n4. Controlling label placement\n#textArea(\n label="Details", \n objectName="project", \n property="details", \n labelPlacement="before"\n)#\n\n5. Prepending and appending HTML\n#textArea(\n label="Notes", \n objectName="task", \n property="notes", \n prepend="<div class='input-wrapper'>", \n append="</div>"\n)#\n\n6. Handling validation errors\n#textArea(\n label="Description", \n objectName="product", \n property="description", \n errorElement="div", \n errorClass="input-error"\n)#\n
"},"hint":"Builds and returns an HTML <textarea> form control for a given model object and property. It is commonly used when you need a larger text input field, such as for descriptions, comments, or notes. The function automatically binds the value of the specified property from the object to the textarea. You can also pass additional attributes like class, id, or rel to customize the generated HTML. When working with nested forms or associations, you can specify the association and position arguments to bind the field to related objects. Wheels also provides options to add labels, control label placement, prepend or append HTML around the field, and handle error display automatically.\n\n","returntype":"string","slug":"controller.textArea","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textArea","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic textarea with label\n#textAreaTag(label="Description", name="description", content=params.description)#\n\n2. Textarea with custom attributes\n#textAreaTag(\n label="Notes", \n name="notes", \n class="form-control", \n id="notesBox", \n rows="6", \n cols="60"\n)#\n\n3. Textarea without label\n#textAreaTag(name="feedback", content="Enter your feedback here...")#\n\n4. Custom label placement\n#textAreaTag(\n label="Comments", \n name="comments", \n labelPlacement="before"\n)#\n\n5. Prepending and appending HTML\n#textAreaTag(\n label="Message", \n name="message", \n prepend="<div class='input-wrapper'>", \n append="</div>"\n)#\n
"},"hint":"Builds and returns an HTML <textarea> form control based only on the supplied field name, rather than being tied to a specific model object. It is useful when you want to generate a standalone text area not bound to an object, such as for ad-hoc forms, search boxes, or generic input fields. You can set the initial content of the textarea, add a label, and pass in additional attributes like class, id, or rel. Options are also available to control label placement, prepend or append HTML wrappers, and configure whether output should be encoded for XSS protection.\n\n","returntype":"string","slug":"controller.textAreaTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Content to display in textarea on page load.","name":"content","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textAreaTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic text field with label\n#textField(label="First Name", objectName="user", property="firstName")#\n\n2. Using a custom input type\n#textField(\n label="Email Address", \n objectName="user", \n property="email", \n type="email"\n)#\n\n3. Adding CSS classes and attributes\n#textField(\n label="Phone", \n objectName="contact", \n property="phoneNumber", \n class="form-control", \n placeholder="Enter phone number"\n)#\n\n4. Nested form with hasMany association\n<fieldset>\n <legend>Phone Numbers</legend>\n <cfloop from="1" to="#ArrayLen(contact.phoneNumbers)#" index="i">\n #textField(\n label="Phone ##i#", \n objectName="contact", \n association="phoneNumbers", \n position=i, \n property="phoneNumber"\n )#\n </cfloop>\n</fieldset>\n\n5. Prepending and appending HTML wrappers\n#textField(\n label="Website", \n objectName="company", \n property="website", \n prepend="<div class='field-wrapper'>", \n append="</div>"\n)#\n\n6. Handling validation errors\n#textField(\n label="Username", \n objectName="user", \n property="username", \n errorElement="div", \n errorClass="input-error"\n)#\n
"},"hint":"Builds and returns an HTML text field form control that is bound to a model object and one of its properties. By default, it will populate the value of the field from the property on the object. You can pass additional attributes such as class, id, or rel to customize the rendered tag. When working with nested associations or hasMany relationships, you can use the association and position arguments to bind the field to related properties. Wheels also supports automatically generating and placing labels, wrapping controls with custom HTML, and marking fields with errors when validation fails. The type argument lets you adjust the input type for use with HTML5 attributes like email, tel, or url.\n\n","returntype":"string","slug":"controller.textField","parameters":[{"required":true,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"required":true,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"useDefaultLabel","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"default":"text","required":false,"hint":"Input type attribute. Common examples in HTML5 and later are text (default), email, tel, and url.","name":"type","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textField","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage with label, name, and value\ntextFieldTag(label="Search", name="q", value=params.q)\n\n2. Email input with placeholder and custom class\ntextFieldTag(name="email", label="Email Address", type="email", class="form-control", placeholder="you@example.com")\n\n3. Label placed after the input\ntextFieldTag(name="username", label="Username", labelPlacement="after")\n\n4. Wrapped with prepend/append for Bootstrap styling\ntextFieldTag(name="price", label="Price", prepend="<div class='input-group'>", append="</div>", prependToLabel="<span class='icon'>$</span>")\n
"},"hint":"Builds and returns a string containing a text field form control based on the supplied name.\nNote: Pass any additional arguments like class, rel, and id, and the generated tag will also include those values as HTML attributes.\n\n","returntype":"string","slug":"controller.textFieldTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value to populate in tag's value attribute.","name":"value","type":"string"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"text","required":false,"hint":"Input type attribute. Common examples in HTML5 and later are text (default), email, tel, and url.","name":"type","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"textFieldTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a simple time column\nt.time("startTime")\n\n2. Add multiple time columns\nt.time("opensAt, closesAt")\n\n3. Add a time column with a default value\nt.time(columnNames="reminderAt", default="09:00:00")\n\n4. Add a nullable time column\nt.time(columnNames="lunchBreak", allowNull=true)\n
"},"hint":"Adds one or more TIME columns to a table definition in a migration. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.time","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"time","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Example in a controller (outputs: "3 months")\naWhileAgo = DateAdd("d", -90, Now());\ntimeAgoInWords(aWhileAgo)\n\n2. Including seconds (outputs: "less than 5 seconds")\ntimeAgoInWords(DateAdd("s", -3, Now()), includeSeconds=true)\n\n3. Comparing two specific dates (Outputs: "5 months")\npast = CreateDateTime(2024, 01, 01, 12, 0, 0);\nfuture = CreateDateTime(2024, 06, 01, 12, 0, 0);\ntimeAgoInWords(fromTime=past, toTime=future)\n
"},"hint":"Returns a human-friendly string describing the approximate time difference between two dates (defaults to comparing against the current time).\n\n","returntype":"any","slug":"controller.timeAgoInWords","parameters":[{"required":true,"hint":"Date to compare from.","name":"fromTime","type":"date"},{"default":false,"required":false,"hint":"Whether or not to include the number of seconds in the returned string.","name":"includeSeconds","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Date to compare to.","name":"toTime","type":"date"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"timeAgoInWords","tags":{"categoryClass":"datefunctions","sectionClass":"globalhelpers","category":"Date Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: create hour, minute, and second selects for a property\ntimeSelect(objectName="business", property="openUntil")\n\n2. Only display hour and minute selectors\ntimeSelect(objectName="business", property="openUntil", order="hour,minute")\n\n3. Limit minutes to 15-minute intervals (00, 15, 30, 45)\ntimeSelect(objectName="appointment", property="dateTimeStart", minuteStep=15)\n\n4. Use 12-hour format with AM/PM\ntimeSelect(objectName="event", property="startTime", twelveHour=true)\n\n5. Add a blank option at the top\ntimeSelect(objectName="schedule", property="startTime", includeBlank="- Select Time -")\n\n6. Customize the label and append helper text\ntimeSelect(objectName="meeting", property="endTime", label="End Time", append="(select carefully)")\n
"},"hint":"Builds and returns three select form controls for hours, minutes, and seconds, based on the supplied object name and property. It is useful when you want users to input a time in a structured way without manually typing values. You can configure it to display only specific units (such as hours and minutes), control step intervals for minutes or seconds, display in 12-hour format with AM/PM, and customize labels, error handling, and additional HTML wrapping. By default, the three selects are ordered as hour, minute, and second, but you can change this order or exclude parts completely.\n\n","returntype":"string","slug":"controller.timeSelect","parameters":[{"default":"","required":false,"hint":"The variable name of the object to build the form control for.","name":"objectName","type":"any"},{"default":"","required":false,"hint":"The name of the property to use in the form control.","name":"property","type":"string"},{"required":false,"hint":"The name of the association that the property is located on. Used for building nested forms that work with nested properties. If you are building a form with deep nesting, simply pass in a list to the nested object, and Wheels will figure it out.","name":"association","type":"string"},{"required":false,"hint":"The position used when referencing a hasMany relationship in the association argument. Used for building nested forms that work with nested properties. If you are building a form with deep nestings, simply pass in a list of positions, and Wheels will figure it out.","name":"position","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"order","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"separator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":false,"required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":"span","required":false,"hint":"HTML tag to wrap the form control with when the object contains errors.","name":"errorElement","type":"string"},{"default":"field-with-errors","required":false,"hint":"The class name of the HTML tag that wraps the form control when there are errors.","name":"errorClass","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"timeSelect","tags":{"categoryClass":"formobjectfunctions","sectionClass":"viewhelpers","category":"Form Object Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: creates hour, minute, and second selects\ntimeSelectTags(name="timeOfMeeting", selected=params.timeOfMeeting)\n\n2. Only show hour and minute selects\ntimeSelectTags(name="timeOfMeeting", selected=params.timeOfMeeting, order="hour,minute")\n\n3. Show 15-minute intervals\ntimeSelectTags(name="reminderTime", minuteStep=15)\n\n4. Display in 12-hour format with AM/PM\ntimeSelectTags(name="eventStart", twelveHour=true)\n\n5. Include a blank option\ntimeSelectTags(name="timeSlot", includeBlank="- Select Time -")\n\n6. Add a label and append helper text\ntimeSelectTags(name="appointmentEnd", label="End Time", append="(HH:MM:SS)")\n
"},"hint":"Builds and returns three <select> form controls for hours, minutes, and seconds based on the supplied name. This is the tag-based version of timeSelect(), meaning it does not bind to a model object but instead works with raw form field names. You can control the order of the selects, limit minute and second intervals, display in 12-hour format with AM/PM, and include custom labels, error handling, and HTML wrappers.\n\n","returntype":"string","slug":"controller.timeSelectTags","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"Value of option that should be selected by default.","name":"selected","type":"string"},{"default":"hour,minute,second","required":false,"hint":"Use to change the order of or exclude time select tags.","name":"order","type":"string"},{"default":":","required":false,"hint":"Use to change the character that is displayed between the time select tags.","name":"separator","type":"string"},{"default":1,"required":false,"hint":"Pass in 10 to only show minute 10, 20, 30, etc.","name":"minuteStep","type":"numeric"},{"default":1,"required":false,"hint":"Pass in 10 to only show seconds 10, 20, 30, etc.","name":"secondStep","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"required":false,"hint":"Set to false to not combine the select parts into a single DateTime object.","name":"combine","type":"boolean"},{"default":false,"required":false,"hint":"whether to display the hours in 24 or 12 hour format. 12 hour format has AM/PM drop downs","name":"twelveHour","type":"boolean"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"}],"availableIn":["controller"],"name":"timeSelectTags","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a basic timestamp column\nt.timestamp("createdAt")\n\n2. Add multiple timestamp columns\nt.timestamp("createdAt, updatedAt")\n\n3. Add a timestamp column with a default value\nt.timestamp(columnNames="createdAt", default="CURRENT_TIMESTAMP")\n\n4. Add a nullable timestamp column\nt.timestamp(columnNames="deletedAt", allowNull=true)\n\n5. Override column type to use TIMESTAMP instead of DATETIME\nt.timestamp(columnNames="syncedAt", columnType="timestamp")\n
"},"hint":"Used to add one or more TIMESTAMP (or DATETIME) columns to a table definition. It lets you specify default values, whether the column allows NULL, and even override the underlying SQL type through the columnType argument. This is especially useful when you need to track creation and update times or work with custom timestamp fields. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.timestamp","parameters":[{"required":false,"name":"columnNames","type":"string"},{"required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"},{"default":"datetime","required":false,"name":"columnType","type":"string"}],"availableIn":["tabledefinition"],"name":"timestamp","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Add createdAt, updatedAt, and deletedAt columns to the users table\nt.timestamps()
"},"hint":"Shortcut for adding Wheels’ convention-based automatic timestamp and soft delete columns to a table definition during migrations. Instead of defining each field manually, this function quickly sets up the standard fields that are commonly used across models to track record lifecycle and soft deletion. By default, it adds createdAt, updatedAt, and deletedAt columns with appropriate types, making your migrations more concise and consistent. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.timestamps","parameters":[],"availableIn":["tabledefinition"],"name":"timestamps","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Example in a controller (outputs: "about 1 year")\naLittleAhead = DateAdd("d", 365, Now());\ntimeUntilInWords(aLittleAhead)\n\n2. Including seconds (outputs: "less than 5 seconds")\ntimeUntilInWords(DateAdd("s", 3, Now()), includeSeconds=true)\n\n3. Comparing between two specific dates (Outputs: "5 months")\nfromDate = CreateDateTime(2024, 01, 01, 12, 0, 0);\ntoDate = CreateDateTime(2024, 06, 01, 12, 0, 0);\ntimeUntilInWords(toTime=toDate, fromTime=fromDate)\n
"},"hint":"Returns a human-readable string describing the approximate time difference between the current date (or another starting point you provide) and a future date. It is the inverse of timeAgoInWords(), focusing on how long until something happens instead of how long ago it occurred. You can optionally include seconds for more precise descriptions.\n\n","returntype":"string","slug":"controller.timeUntilInWords","parameters":[{"required":true,"hint":"Date to compare to.","name":"toTime","type":"date"},{"default":false,"required":false,"hint":"Whether or not to include the number of seconds in the returned string.","name":"includeSeconds","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Date to compare from.","name":"fromTime","type":"date"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"timeUntilInWords","tags":{"categoryClass":"datefunctions","sectionClass":"globalhelpers","category":"Date Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage\ntitleize(\"Wheels is a framework for ColdFusion\")\n// Output: \"Wheels Is A Framework For ColdFusion\"\n\n2. Works with single words\ntitleize(\"hello\")\n// Output: \"Hello\"\n\n3. Works with multiple words including numbers\ntitleize(\"coldfusion 2025 features\")\n// Output: \"Coldfusion 2025 Features\"\n\n4. Can be used in views for dynamic labels\n<h1>titleize(article.title)</h1>
"},"hint":"Converts a string so that the first letter of each word is capitalized, producing a cleaner, title-like appearance. It is useful for formatting headings, labels, or any text that should follow title case conventions.\n\n","returntype":"string","slug":"controller.titleize","parameters":[{"required":true,"hint":"The text to turn into a title.","name":"word","type":"string"}],"availableIn":["controller","model","test","mapper","migrator","migration","tabledefinition"],"name":"titleize","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Fetch a user object and toggle a boolean property\nuser = model(\"user\").findByKey(58);\nisSuccess = user.toggle(\"isActive\");\n// Returns true if saved successfully, false otherwise\n\n2. Disable automatic saving\nuser = model(\"user\").findByKey(58);\nuser.toggle(property=\"isActive\", save=false);\n// Returns the user object without saving\n\n3. Use a dynamic helper method for convenience\nuser = model(\"user\").findByKey(58);\nisSuccess = user.toggleIsActive();\n// Returns whether the save was successful
"},"hint":"Assigns to the property specified the opposite of the property's current boolean value.\nThrows an error if the property cannot be converted to a boolean value.\nReturns this object if save called internally is false.\n\n","returntype":"boolean","slug":"model.toggle","parameters":[{"required":true,"name":"property","type":"string"},{"default":true,"required":false,"hint":"Argument to decide whether save the property after it has been toggled.","name":"save","type":"boolean"}],"availableIn":["model"],"name":"toggle","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Truncate text to 20 characters, default truncation string \"...\"\ntruncate(text=\"Wheels is a framework for ColdFusion\", length=20)\n/* Output: \"Wheels is a fra...\" */\n\n2. Use a custom truncation string\ntruncate(text=\"Wheels is a framework for ColdFusion\", truncateString=\" (more)\")\n/* Output: \"Wheels is a framework (more)\" */\n\n3. Short text does not get truncated\ntruncate(text=\"Short text\", length=20)\n/* Output: \"Short text\" */\n\n4. Display in a view for previews\n<p>truncate(article.content, 100)</p>
"},"hint":"Shortens a given text string to a specified length and appends a replacement string (by default \"...\") at the end to indicate truncation. It is useful for displaying previews of longer text in UIs, summaries, or reports while keeping the output concise.\n\n","returntype":"string","slug":"controller.truncate","parameters":[{"required":true,"hint":"The text to truncate.","name":"text","type":"string"},{"default":30,"required":false,"hint":"Length to truncate the text to.","name":"length","type":"numeric"},{"default":"...","required":false,"hint":"String to replace the last characters with.","name":"truncateString","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"truncate","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Add a single UUID column\nt.uniqueidentifier("uuid")\n\n2. Add multiple UUID columns\nt.uniqueidentifier("uuid, externalId")\n\n3. Add a UUID column with default UUID generation\nt.uniqueidentifier(columnNames="uuid", default="newid()")\n\n4. Add a nullable UUID column\nt.uniqueidentifier(columnNames="optionalUuid", allowNull=true)\n
"},"hint":"Used to add one or more UUID (Universally Unique Identifier) columns to a table definition. These columns are useful for generating globally unique keys for records instead of relying on auto-incrementing integers. By default, the function uses newid() to populate the column with a UUID, and you can also configure whether the column allows NULL. Only available in a migrator CFC.\n\n","returntype":"any","slug":"tabledefinition.uniqueidentifier","parameters":[{"required":false,"name":"columnNames","type":"string"},{"default":"newid()","required":false,"name":"default","type":"string"},{"required":false,"name":"allowNull","type":"boolean"}],"availableIn":["tabledefinition"],"name":"uniqueidentifier","tags":{"categoryClass":"tabledefinitionfunctions","sectionClass":"migrator","category":"Table Definition Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
function up() {\n\ttransaction {\n\t\ttry {\n\t\t\t// your code goes here\n\t\t\tt = createTable(name='myTable');\n\t\t\tt.timestamps();\n\t\t\tt.create();\n\t\t} catch (any e) {\n\t\t\tlocal.exception = e;\n\t\t}\n\n\t\tif (StructKeyExists(local, "exception")) {\n\t\t\ttransaction action="rollback";\n\t\t\tthrow(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any");\n\t\t} else {\n\t\t\ttransaction action="commit";\n\t\t}\n\t}\n}\n
"},"hint":"Defines the actions to migrate your database schema forward. It is called when applying a migration and is typically paired with the down() function, which rolls back the migration. All schema changes, such as creating tables, adding columns, or setting up indexes, should be placed inside up(). Wrapping your migration code in a transaction block ensures that changes are either fully applied or rolled back in case of errors. Only available in a migration CFC.\n\n","returntype":"void","slug":"migration.up","parameters":[],"availableIn":["migration"],"name":"up","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Get a post object and then update its title in the database\npost = model("post").findByKey(33);\npost.update(title="New version of Wheels just released");\n\n2. Get a post object and then update its title and other properties based on what is pased in from the URL/form\npost = model("post").findByKey(params.key);\npost.update(title="New version of Wheels just released", properties=params.post);\n\n3. If you have a `hasOne` association setup from `author` to `bio`, you can do a scoped call. (The `setBio` method below will call `bio.update(authorId=anAuthor.id)` internally.)\nauthor = model("author").findByKey(params.authorId); \nbio = model("bio").findByKey(params.bioId); \nauthor.setBio(bio); \n\n4. If you have a `hasMany` association setup from `owner` to `car`, you can do a scoped call. (The `addCar` method below will call `car.update(ownerId=anOwner.id)` internally.)\nanOwner = model("owner").findByKey(params.ownerId); \naCar = model("car").findByKey(params.carId); \nanOwner.addCar(aCar); \n\n5. If you have a `hasMany` association setup from `post` to `comment`, you can do a scoped call. (The `removeComment` method below will call `comment.update(postId="")` internally.)\naPost = model("post").findByKey(params.postId); \naComment = model("comment").findByKey(params.commentId); \naPost.removeComment(aComment); // Get an object, and toggle a boolean property\nuser = model("user").findByKey(58); \nisSuccess = user.toggle("isActive"); // returns whether the object was saved properly\n\n// You can also use a dynamic helper for this\nisSuccess = user.toggleIsActive(); \n\n
"},"hint":"Updates an existing model object with the supplied properties and saves the changes to the database. It returns true if the save was successful and false otherwise. You can pass properties directly as named arguments or as a struct. Additional options allow you to control validation, callbacks, transactions, parameterization, cache reloading, and explicit timestamp handling. This method also works seamlessly with associations, making it possible to update related objects in hasOne or hasMany relationships.\n\n","returntype":"boolean","slug":"model.update","parameters":[{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":true,"required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"hint":"Set this to `true` to allow explicit assignment of `updatedAt` property","name":"allowExplicitTimestamps","type":"boolean"}],"availableIn":["model"],"name":"update","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Update all posts that are unpublished\nrecordsUpdated = model("post").updateAll(\n published=1,\n publishedAt=Now(),\n where="published=0"\n)\n\n2. Scoped update for a hasMany association (removing all comments)\npost = model("post").findByKey(params.postId);\npost.removeAllComments(); \n// Internally calls: model("comment").updateAll(postId="", where="postId=#post.id#")\n\n3. Update all users and force validations and callbacks\nrecordsUpdated = model("user").updateAll(\n properties={isActive=true},\n instantiate=true,\n validate=true\n)\n\n4. Using index hints for MySQL\nrecordsUpdated = model("user").updateAll(\n properties={isVerified=true},\n where="isVerified=0",\n useIndex={user="idx_users_isVerified"}\n)\n
"},"hint":"Updates all properties for the records that match the where argument.\nProperty names and values can be passed in either using named arguments or as a struct to the properties argument.\nBy default, objects will not be instantiated and therefore callbacks and validations are not invoked.\nYou can change this behavior by passing in instantiate=true.\nThis method returns the number of records that were updated.\n\n","returntype":"numeric","slug":"model.updateAll","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Associations that should be included in the query using `INNER` or `LEFT OUTER` joins (which join type that is used depends on how the association has been set up in your model). If all included associations are set on the current model, you can specify them in a list (e.g. `department,addresses,emails`). You can build more complex include strings by using parentheses when the association is set on an included model, like `album(artist(genre))`, for example. These complex `include` strings only work when `returnAs` is set to `query` though.","name":"include","type":"string"},{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":false,"required":false,"hint":"Whether or not to instantiate the object(s) first. When objects are not instantiated, any callbacks and validations set on them will be skipped.","name":"instantiate","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"updateAll","tags":{"categoryClass":"updatefunctions","sectionClass":"modelclass","category":"Update Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Update a record by key using a struct of properties\nresult = model("post").updateByKey(33, params.post);\n// Returns true if the update was successful\n\n2. Update a record by key using named arguments\nresult = model("post").updateByKey(\n key=33,\n title="New version of Wheels just released",\n published=1\n)\n\n3. Include soft-deleted records in the update\nresult = model("user").updateByKey(\n key=42,\n properties={isActive=true},\n includeSoftDeletes=true\n)\n\n4. Disable validation and callbacks\nresult = model("post").updateByKey(\n key=33,\n properties={title="Force Update"},\n validate=false,\n callbacks=false\n)\n
"},"hint":"Finds the object with the supplied key and saves it (if validation permits it) with the supplied properties and / or named arguments.\nProperty names and values can be passed in either using named arguments or as a struct to the properties argument.\nReturns true if the object was found and updated successfully, false otherwise.\n\n","returntype":"boolean","slug":"model.updateByKey","parameters":[{"required":true,"hint":"Primary key value(s) of the record to fetch. Separate with comma if passing in multiple primary key values. Accepts a string, list, or a numeric value.","name":"key","type":"any"},{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"false","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"updateByKey","tags":{"categoryClass":"updatefunctions","sectionClass":"modelclass","category":"Update Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Sets the `new` property to `1` on the most recently released product\nresult = model("product").updateOne(order="releaseDate DESC", new=1);\n\n2. If you have a `hasOne` association setup from `user` to `profile`, you can do a scoped call. (The `removeProfile` method below will call `model("profile").updateOne(where="userId=#aUser.id#", userId="")` internally.)\naUser = model("user").findByKey(params.userId);\naUser.removeProfile();
"},"hint":"Retrieves a single model object based on the supplied arguments and updates it with the specified properties. It returns true if an object was found and updated successfully, and false if no object matched the criteria or the update failed. This method is useful when you want to update a single record that matches a certain condition without fetching multiple records. By default, objects are not instantiated, so validations and callbacks are applied only if enabled. Additional options allow control over query ordering, transactions, cache reloading, index hints, and inclusion of soft-deleted records.\n\n","returntype":"boolean","slug":"model.updateOne","parameters":[{"default":"","required":false,"hint":"Maps to the `WHERE` clause of the query (or `HAVING` when necessary). The following operators are supported: `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `AND`, and `OR` (note that the key words need to be written in upper case). You can also use parentheses to group statements. Nested queries not allowed. You do not need to specify the table name(s); Wheels will do that for you.","name":"where","type":"string"},{"default":"","required":false,"hint":"Maps to the `ORDER` BY clause of the query. You do not need to specify the table name(s); Wheels will do that for you.","name":"order","type":"string"},{"default":"[runtime expression]","required":false,"hint":"The properties you want to set on the object (can also be passed in as named arguments).","name":"properties","type":"struct"},{"default":false,"required":false,"hint":"Set to `true` to force Wheels to query the database even though an identical query for this model may have been run in the same request. (The default in Wheels is to get the second query from the model's request-level cache.)","name":"reload","type":"boolean"},{"default":"true","required":false,"hint":"Set to `false` to skip validations for this operation.","name":"validate","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":"[runtime expression]","required":false,"hint":"If you want to specify table index hints, pass in a structure of index names using your model names as the structure keys. Eg: `{user=\"idx_users\", post=\"idx_posts\"}`. This feature is only supported by MySQL and SQL Server.","name":"useIndex","type":"struct"},{"default":"false","required":false,"name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"updateOne","tags":{"categoryClass":"updatefunctions","sectionClass":"modelclass","category":"Update Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Update a single property on an existing product\nproduct = model(\"product\").findByKey(56);\nproduct.updateProperty(\"new\", 1);\n\n2. Update a boolean flag without callbacks or validations\nuser = model(\"user\").findByKey(42);\nuser.updateProperty(property=\"isActive\", value=false, callbacks=false);
"},"hint":"Updates a single property on a model object and saves the record immediately without running the normal validation procedures. This method is particularly useful for quickly updating flags or boolean values on existing records where full validation is not necessary. You can control transaction behavior, parameterization, and callback execution when using this method.\n\n","returntype":"boolean","slug":"model.updateProperty","parameters":[{"required":false,"hint":"Name of the property to update the value for globally.","name":"property","type":"string"},{"required":false,"hint":"Value to set on the given property globally.","name":"value","type":"any"},{"default":true,"required":false,"hint":"Set to `true` to use `cfqueryparam` on all columns, or pass in a list of property names to use `cfqueryparam` on those only.","name":"parameterize","type":"any"},{"default":"[runtime expression]","required":false,"hint":"Set this to `commit` to update the database, `rollback` to run all the database queries but not commit them, or `none` to skip transaction handling altogether.","name":"transaction","type":"string"},{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"}],"availableIn":["model"],"name":"updateProperty","tags":{"categoryClass":"crudfunctions","sectionClass":"modelobject","category":"CRUD Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Update the `active` column to 0 for all admin users in a migration\nupdateRecord(table=\"users\", where=\"admin = 1\", active=0);\n\n2. Update a specific product record by ID\nupdateRecord(table=\"products\", where=\"id = 42\", price=19.99, stock=100);
"},"hint":"Allows you to update an existing record in a database table directly from within a migration CFC. This function is particularly useful when you need to modify data as part of a schema migration, such as setting default values, correcting legacy data, or updating specific records based on certain conditions. The function requires the table name and optionally allows a where clause to target specific rows. Only available in a migrator CFC.\n\n","returntype":"void","slug":"migration.updateRecord","parameters":[{"required":true,"hint":"The table name where the record is","name":"table","type":"string"},{"default":"","required":false,"hint":"The where clause, i.e admin = 1","name":"where","type":"string"}],"availableIn":["migration"],"name":"updateRecord","tags":{"categoryClass":"migrationfunctions","sectionClass":"migrator","category":"Migration Functions","section":"Migrator"}},{"extended":{"hasExtended":true,"docs":"
1. Create the URL for the `logOut` action on the `account` controller, typically resulting in `/account/log-out`\n#urlFor(controller="account", action="logOut")#\n\n2. Create a URL with an anchor set on it\n#urlFor(action="comments", anchor="comment10")#\n\n3. Create a URL based on a route called `products`, which expects params for `categorySlug` and `productSlug`\n#urlFor(route="product", categorySlug="accessories", productSlug="battery-charger")#
"},"hint":"Generates an internal URL based on the supplied arguments. It can create URLs using a named route, or by specifying a controller and action directly. Additional options let you include keys, query parameters, anchors, and override protocol, host, or port. By default, the function returns a relative URL, but you can configure it to return a fully qualified URL. URL parameters are automatically encoded for safety, but for HTML attribute safety, further encoding is recommended.\n\n","returntype":"string","slug":"controller.URLFor","parameters":[{"default":"","required":false,"hint":"Name of a route that you have configured in `config/routes.cfm`.","name":"route","type":"string"},{"default":"","required":false,"hint":"Name of the controller to include in the URL.","name":"controller","type":"string"},{"default":"","required":false,"hint":"Name of the action to include in the URL.","name":"action","type":"string"},{"default":"","required":false,"hint":"Key(s) to include in the URL.","name":"key","type":"any"},{"default":"","required":false,"hint":"Any additional parameters to be set in the query string (example: `wheels=cool&x=y`). Please note that Wheels uses the `&` and `=` characters to split the parameters and encode them properly for you. However, if you need to pass in `&` or `=` as part of the value, then you need to encode them (and only them), example: `a=cats%26dogs%3Dtrouble!&b=1`.","name":"params","type":"string"},{"default":"","required":false,"hint":"Sets an anchor name to be appended to the path.","name":"anchor","type":"string"},{"default":true,"required":false,"hint":"If `true`, returns only the relative URL (no protocol, host name or port).","name":"onlyPath","type":"boolean"},{"default":"","required":false,"hint":"Set this to override the current host.","name":"host","type":"string"},{"default":"","required":false,"hint":"Set this to override the current protocol.","name":"protocol","type":"string"},{"default":0,"required":false,"hint":"Set this to override the current port number.","name":"port","type":"numeric"},{"default":true,"required":false,"hint":"Encode URL parameters using `EncodeForURL()`. Please note that this does not make the string safe for placement in HTML attributes, for that you need to wrap the result in `EncodeForHtmlAttribute()` or use `linkTo()`, `startFormTag()` etc instead.","name":"encode","type":"boolean"},{"default":false,"required":false,"name":"$encodeForHtmlAttribute","type":"boolean"},{"default":"[runtime expression]","required":false,"name":"$URLRewriting","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"URLFor","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"globalhelpers","category":"Miscellaneous Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. We want this layout to be used as the default throughout the entire controller, except for the `myAjax` action. \nusesLayout(template="myLayout", except="myAjax"); \n\n2. Use a custom layout for these actions but use the default `layout.cfm` for the rest. \nusesLayout(template="myLayout", only="termsOfService,shippingPolicy"); \n\n3. Define a custom function to decide which layout to display.\n// The `setLayout` function should return the name of the layout to use or `true` to use the default one. \nusesLayout("setLayout");
"},"hint":"Used inside a controller's config() function to specify which layout template should be applied to the controller or specific actions. You can define a default layout for the entire controller, specify layouts only for certain actions, exclude specific actions from using a layout, or even provide a custom function to determine which layout to use dynamically. This allows fine-grained control over your page structure and helps maintain consistent design while accommodating exceptions.\n\n","returntype":"void","slug":"controller.usesLayout","parameters":[{"required":true,"hint":"Name of the layout template or function name you want to use.","name":"template","type":"string"},{"default":"","required":false,"hint":"Name of the layout template you want to use for AJAX requests.","name":"ajax","type":"string"},{"required":false,"hint":"List of actions that should not get the layout.","name":"except","type":"string"},{"required":false,"hint":"List of actions that should only get the layout.","name":"only","type":"string"},{"default":true,"required":false,"hint":"When specifying conditions or a function, pass in `true` to use the default `layout.cfm` if none of the conditions are met.","name":"useDefault","type":"boolean"}],"availableIn":["controller"],"name":"usesLayout","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
// Check if a user is valid before proceeding with execution\nuser = model("user").new(params.user);\n\nif(user.valid()){\n    // Do something here\n}
"},"hint":"Runs the validation on the object and returns true if it passes it.\nWheels will run the validation process automatically whenever an object is saved to the database, but sometimes it's useful to be able to run this method to see if the object is valid without saving it to the database.\n\n","returntype":"boolean","slug":"model.valid","parameters":[{"default":"true","required":false,"hint":"Set to `false` to disable callbacks for this method.","name":"callbacks","type":"boolean"},{"default":false,"required":false,"name":"validateAssociations","type":"boolean"}],"availableIn":["model"],"name":"valid","tags":{"categoryClass":"errorfunctions","sectionClass":"modelobject","category":"Error Functions","section":"Model Object"}},{"extended":{"hasExtended":true,"docs":"
1. Register a method to validate objects before saving\nfunction config() {\n validate("checkPhoneNumber");\n}\n\nfunction checkPhoneNumber() {\n // Make sure area code is '614'\n return Left(this.phoneNumber, 3) == "614";\n}\n\n2. Register multiple validation methods\nfunction config() {\n validate("checkPhoneNumber, checkEmailFormat");\n}\n\nfunction checkEmailFormat() {\n // Ensure email contains '@'\n return Find("@", this.email);\n}\n\n3. Conditional validation using `condition`\nfunction config() {\n // Only validate phone numbers if the user is in the US\n validate("checkPhoneNumber", condition="this.country == 'US'");\n}\n\n4. Skip validation under certain conditions using `unless`\nfunction config() {\n // Skip phone number validation if the user is a guest\n validate("checkPhoneNumber", unless="this.isGuest");\n}\n\n5. Run validation only on create or update\nfunction config() {\n // Validate email only when creating a new record\n validate("checkEmailFormat", when="onCreate");\n\n // Validate password only on update\n validate("checkPasswordStrength", when="onUpdate");\n}\n
"},"hint":"Used to register one or more validation methods that will be executed on a model object before it is saved to the database. This allows you to define custom validation logic beyond the built-in validations like presence or uniqueness. You can also control when the validation runs (on create, update, or both) and under what conditions using condition and unless.\n\n","returntype":"void","slug":"model.validate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names to call. Can also be called with the `method` argument.","name":"methods","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"}],"availableIn":["model"],"name":"validate","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Validate new objects before insertion\nfunction config() {\n validateOnCreate("checkPhoneNumber");\n}\n\nfunction checkPhoneNumber() {\n // Ensure area code is '614'\n return Left(this.phoneNumber, 3) == "614";\n}\n\n2. Register multiple methods for validation on creation\nfunction config() {\n validateOnCreate("checkPhoneNumber, checkEmailFormat");\n}\n\nfunction checkEmailFormat() {\n // Ensure email contains '@'\n return Find("@", this.email);\n}\n\n3. Conditional validation using `condition`\nfunction config() {\n // Only validate phone number if the country is US\n validateOnCreate("checkPhoneNumber", condition="this.country == 'US'");\n}\n\n4. Skip validation under certain conditions using `unless`\nfunction config() {\n // Skip phone number validation if user is a guest\n validateOnCreate("checkPhoneNumber", unless="this.isGuest");\n}\n
"},"hint":"Registers one or more validation methods that will be executed only when a new object is being inserted into the database. This is useful for rules that should apply strictly at creation and not during updates. You can also control whether the validation runs using the condition and unless arguments.\n\n","returntype":"void","slug":"model.validateOnCreate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names to call. Can also be called with the `method` argument.","name":"methods","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validateOnCreate","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic usage: validate existing objects before update\nfunction config() {\n validateOnUpdate("checkPhoneNumber");\n}\n\nfunction checkPhoneNumber() {\n // Ensure area code is '614'\n return Left(this.phoneNumber, 3) == "614";\n}\n\n2. Register multiple methods for validation on update\nfunction config() {\n validateOnUpdate("checkPhoneNumber, checkEmailFormat");\n}\n\nfunction checkEmailFormat() {\n // Ensure email contains '@'\n return Find("@", this.email);\n}\n\n3. Conditional validation using `condition`\nfunction config() {\n // Only validate phone number if the country is US\n validateOnUpdate("checkPhoneNumber", condition="this.country == 'US'");\n}\n\n4. Skip validation under certain conditions using `unless`\nfunction config() {\n // Skip phone number validation if the user is an admin\n validateOnUpdate("checkPhoneNumber", unless="this.isAdmin");\n}\n
"},"hint":"Registers one or more validation methods that will be executed only when an existing object is being updated in the database. This allows you to enforce rules that apply strictly to updates, without affecting the creation of new records. You can also control whether the validation runs using the condition and unless arguments.\n\n","returntype":"void","slug":"model.validateOnUpdate","parameters":[{"default":"","required":false,"hint":"Method name or list of method names to call. Can also be called with the `method` argument.","name":"methods","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validateOnUpdate","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Validate password confirmation on user creation\nvalidatesConfirmationOf(\n    property="password",\n    when="onCreate",\n    message="Your password and its confirmation do not match. Please try again."\n);\n\n2. Validate multiple fields at once: password and email\nvalidatesConfirmationOf(\n    properties="password,email",\n    when="onCreate",\n    message="Fields must match their confirmation."\n);\n\n3. Case-sensitive validation\nvalidatesConfirmationOf(\n    property="password",\n    caseSensitive=true,\n    message="Password and confirmation must match exactly, including case."\n);\n\n4. Conditional validation using `condition`\nvalidatesConfirmationOf(\n    property="email",\n    condition="this.isNewsletterSubscriber",\n    message="Email confirmation required for newsletter subscription."\n);\n\n5. Skip validation for admin users using `unless`\nvalidatesConfirmationOf(\n    property="password",\n    unless="this.isAdmin",\n    message="Admins do not need to confirm their password."\n);\n
"},"hint":"Validates that the value of the specified property also has an identical confirmation value.\nThis is common when having a user type in their email address a second time to confirm, confirming a password by typing it a second time, etc.\nThe confirmation value only exists temporarily and never gets saved to the database.\nBy convention, the confirmation property has to be named the same as the property with \"Confirmation\" appended at the end.\nUsing the password example, to confirm our password property, we would create a property called passwordConfirmation.\n\n","returntype":"void","slug":"model.validatesConfirmationOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] should match confirmation","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":false,"required":false,"hint":"Ensure the confirmed property comparison is case sensitive","name":"caseSensitive","type":"boolean"}],"availableIn":["model"],"name":"validatesConfirmationOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Prevent users from selecting certain programming languages\nvalidatesExclusionOf(\n    property="coolLanguage",\n    list="php,fortran",\n    message="Haha, you can not be serious. Try again, please."\n);\n\n2. Validate multiple properties at once\nvalidatesExclusionOf(\n    properties="username,email",\n    list="admin,root,system",\n    message="This value is reserved. Please choose another."\n);\n\n3. Only apply validation on object creation\nvalidatesExclusionOf(\n    property="username",\n    list="admin,root",\n    when="onCreate",\n    message="Username is reserved and cannot be used."\n);\n\n4. Skip validation if the property is blank\nvalidatesExclusionOf(\n    property="nickname",\n    list="boss,chief",\n    allowBlank=true\n);\n\n5. Conditional validation using `condition`\nvalidatesExclusionOf(\n    property="category",\n    list="deprecated,legacy",\n    condition="this.isArchived",\n    message="Archived items cannot use deprecated categories."\n);\n\n6. Skip validation for admin users using `unless`\nvalidatesExclusionOf(\n    property="role",\n    list="banned,guest",\n    unless="this.isAdmin",\n    message="This role is restricted for regular users."\n);\n
"},"hint":"Ensures that the value of a specified property is not included in a given list of disallowed values. This is commonly used to prevent reserved words, restricted entries, or disallowed values from being saved to the database. You can specify when the validation should run, allow blank values to skip validation, or conditionally run it.\n\n","returntype":"void","slug":"model.validatesExclusionOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"required":true,"hint":"Single value or list of values that should not be allowed.","name":"list","type":"string"},{"default":"[property] is reserved","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesExclusionOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Validate that a credit card number is correct\nvalidatesFormatOf(property="cc", type="creditcard");\n\n2. Validate that a US zipcode matches 5 or 9 digit format\nvalidatesFormatOf(property="zipcode", type="zipcode");\n\n3. Ensure that an email ends with `.se` when IP check returns true and today is not Sunday\nvalidatesFormatOf(\n    property="email",\n    regEx="^.*@.*\\.se$",\n    condition="ipCheck()",\n    unless="DayOfWeek() eq 1",\n    message="Sorry, you must have a Swedish email address to use this website."\n);\n\n4. Validate that a username contains only letters, numbers, or underscores\nvalidatesFormatOf(\n    property="username",\n    regEx="^[a-zA-Z0-9_]+$",\n    message="Username can only contain letters, numbers, and underscores."\n);\n\n5. Validate multiple properties at once using built-in CFML types\nvalidatesFormatOf(\n    properties="phone,email",\n    type="telephone,email",\n    allowBlank=true\n);\n\n6. Validate only when updating an existing object\nvalidatesFormatOf(\n    property="ssn",\n    type="social_security_number",\n    when="onUpdate",\n    message="Invalid SSN format for updating records."\n);\n
"},"hint":"Validates that the value of the specified property is formatted correctly by matching it against a regular expression using the regEx argument and / or against a built-in CFML validation type using the type argument (creditcard, date, email, etc.).\n\n","returntype":"void","slug":"model.validatesFormatOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"","required":false,"hint":"Regular expression to verify against.","name":"regEx","type":"string"},{"default":"","required":false,"hint":"One of the following types to verify against: creditcard, date, email, eurodate, guid, social_security_number, ssn, telephone, time, URL, USdate, UUID, variableName, zipcode (will be passed through to your CFML engine's IsValid() function).","name":"type","type":"string"},{"default":"[property] is invalid","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesFormatOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure that the user selects either "Wheels" or "Rails" as their framework\nvalidatesInclusionOf(\n    property="frameworkOfChoice",\n    list="wheels,rails",\n    message="Please try again, and this time, select a decent framework!"\n);\n\n2. Validate multiple properties at once\nvalidatesInclusionOf(\n    properties="frameworkOfChoice,editorChoice",\n    list="wheels,rails,vsCode,sublime",\n    message="Invalid selection."\n);\n\n3. Only validate when creating a new object\nvalidatesInclusionOf(\n    property="subscriptionType",\n    list="free,premium,enterprise",\n    when="onCreate",\n    message="You must choose a valid subscription type."\n);\n\n4. Skip validation if property is blank\nvalidatesInclusionOf(\n    property="preferredLanguage",\n    list="cfml,python,javascript",\n    allowBlank=true\n);\n\n5. Conditionally validate only for users in Europe\nvalidatesInclusionOf(\n    property="currency",\n    list="EUR,GBP,CHF",\n    condition="this.region eq 'Europe'",\n    message="Invalid currency for European users."\n);\n
"},"hint":"Ensures that a property’s value exists in a predefined list of allowed values. It is commonly used for dropdowns, radio buttons, or any scenario where only specific values are acceptable.\n\n","returntype":"void","slug":"model.validatesInclusionOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"required":true,"hint":"List of allowed values.","name":"list","type":"string"},{"default":"[property] is not included in the list","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesInclusionOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure that `firstName` and `lastName` are no more than 50 characters\nvalidatesLengthOf(\n    properties="firstName,lastName",\n    maximum=50,\n    message="Please shorten your [property] please (50 characters max)."\n);\n\n2. Ensure `password` is between 4 and 20 characters\nvalidatesLengthOf(\n    property="password",\n    within="4,20",\n    message="The password length must be between 4 and 20 characters."\n);\n\n3. Ensure `username` is exactly 8 characters\nvalidatesLengthOf(\n    property="username",\n    exactly=8,\n    message="Username must be exactly 8 characters."\n);\n\n4. Only validate if `region` is 'US'\nvalidatesLengthOf(\n    property="zipCode",\n    exactly=5,\n    condition="this.region eq 'US'",\n    message="US zip codes must be exactly 5 digits."\n);\n\n5. Skip validation if property is blank\nvalidatesLengthOf(\n    property="nickname",\n    maximum=15,\n    allowBlank=true\n);\n
"},"hint":"Validates that the value of the specified property matches the length requirements supplied.\nUse the exactly, maximum, minimum and within arguments to specify the length requirements.\n\n","returntype":"void","slug":"model.validatesLengthOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] is the wrong length","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":0,"required":false,"hint":"The exact length that the property value must be.","name":"exactly","type":"numeric"},{"default":0,"required":false,"hint":"The maximum length that the property value can be.","name":"maximum","type":"numeric"},{"default":0,"required":false,"hint":"The minimum length that the property value can be.","name":"minimum","type":"numeric"},{"default":"","required":false,"hint":"A list of two values (minimum and maximum) that the length of the property value must fall within.","name":"within","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesLengthOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Score must be an integer, but allow blank values\nvalidatesNumericalityOf(\n    property="score",\n    onlyInteger=true,\n    allowBlank=true,\n    message="Please enter a correct score."\n);\n\n2. Age must be a number greater than or equal to 18\nvalidatesNumericalityOf(\n    property="age",\n    greaterThanOrEqualTo=18,\n    message="You must be at least 18 years old."\n);\n\n3. Price must be a positive number less than 1000\nvalidatesNumericalityOf(\n    property="price",\n    greaterThan=0,\n    lessThan=1000,\n    message="Price must be between 0 and 1000."\n);\n\n4. Ensure a number is odd and an integer\nvalidatesNumericalityOf(\n    property="lotteryNumber",\n    odd=true,\n    onlyInteger=true,\n    message="Lottery number must be an odd integer."\n);\n\n5. Validate only when a specific condition is true\nvalidatesNumericalityOf(\n    property="discount",\n    greaterThanOrEqualTo=0,\n    lessThanOrEqualTo=50,\n    condition="this.isOnSale()",\n    message="Discount must be between 0 and 50 for sale items."\n);\n
"},"hint":"Ensures that a property’s value is numeric. You can also enforce additional constraints such as integer-only values, odd/even numbers, and comparison limits (greaterThan, lessThan, etc.).\n\n","returntype":"void","slug":"model.validatesNumericalityOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] is not a number","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":false,"required":false,"hint":"Specifies whether the property value must be an integer.","name":"onlyInteger","type":"boolean"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":"","required":false,"name":"odd","type":"boolean"},{"default":"","required":false,"name":"even","type":"boolean"},{"default":"","required":false,"hint":"Specifies whether or not the value must be greater than the supplied value.","name":"greaterThan","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be greater than or equal the supplied value.","name":"greaterThanOrEqualTo","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be equal to the supplied value.","name":"equalTo","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be less than the supplied value.","name":"lessThan","type":"numeric"},{"default":"","required":false,"hint":"Specifies whether or not the value must be less than or equal the supplied value.","name":"lessThanOrEqualTo","type":"numeric"}],"availableIn":["model"],"name":"validatesNumericalityOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure the `emailAddress` property is not blank\nvalidatesPresenceOf("emailAddress");\n\n2. Ensure multiple properties are present\nvalidatesPresenceOf("firstName,lastName,emailAddress");\n\n3. Use a custom error message for missing email\nvalidatesPresenceOf(\n    property="emailAddress",\n    message="Email is required to create your account."\n);\n\n4. Validate only on create, not on update\nvalidatesPresenceOf(\n    properties="password",\n    when="onCreate",\n    message="Password is required when registering a new user."\n);\n\n5. Conditional validation based on a method\nvalidatesPresenceOf(\n    properties="discountCode",\n    condition="this.isOnSale()",\n    message="Discount code must be present for sale items."\n);\n
"},"hint":"Ensures that the specified property (or properties) exists and is not blank. It is commonly used to enforce required fields before saving an object to the database.\n\n","returntype":"void","slug":"model.validatesPresenceOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] can't be empty","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"}],"availableIn":["model"],"name":"validatesPresenceOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Ensure that usernames are unique across all users\nvalidatesUniquenessOf(\n    property="username",\n    message="Sorry, that username is already taken."\n);\n\n2. Ensure that email addresses are unique\nvalidatesUniquenessOf(\n    property="emailAddress",\n    message="This email has already been registered."\n);\n\n3. Allow the same username in different accounts but unique within an account\nvalidatesUniquenessOf(\n    property="username",\n    scope="accountId",\n    message="This username is already used in this account."\n);\n\n4. Only enforce uniqueness if the user is active\nvalidatesUniquenessOf(\n    property="username",\n    condition="this.isActive",\n    message="Active users must have a unique username."\n);\n\n5. Skip uniqueness check if the field is blank\nvalidatesUniquenessOf(\n    property="nickname",\n    allowBlank=true,\n    message="Nickname must be unique if supplied."\n);\n
"},"hint":"Validates that the value of the specified property is unique in the database table.\nUseful for ensuring that two users can't sign up to a website with identical usernames for example.\nWhen a new record is created, a check is made to make sure that no record already exists in the database table with the given value for the specified property.\nWhen the record is updated, the same check is made but disregarding the record itself.\n\n","returntype":"void","slug":"model.validatesUniquenessOf","parameters":[{"default":"","required":false,"hint":"Name of property or list of property names to validate against (can also be called with the `property` argument).","name":"properties","type":"string"},{"default":"[property] has already been taken","required":false,"hint":"Supply a custom error message here to override the built-in one.","name":"message","type":"string"},{"default":"onSave","required":false,"hint":"Pass in `onCreate` or `onUpdate` to limit when this validation occurs (by default validation will occur on both create and update, i.e. `onSave`).","name":"when","type":"string"},{"default":false,"required":false,"hint":"If set to `true`, validation will be skipped if the property value is an empty string or doesn't exist at all. This is useful if you only want to run this validation after it passes the `validatesPresenceOf` test, thus avoiding duplicate error messages if it doesn't.","name":"allowBlank","type":"boolean"},{"default":"","required":false,"hint":"One or more properties by which to limit the scope of the uniqueness constraint.","name":"scope","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `true` validation will run).","name":"condition","type":"string"},{"default":"","required":false,"hint":"String expression to be evaluated that decides if validation will be run (if the expression returns `false` validation will run).","name":"unless","type":"string"},{"default":"true","required":false,"hint":"Set to `true` to include soft-deleted records in the queries that this method runs.","name":"includeSoftDeletes","type":"boolean"}],"availableIn":["model"],"name":"validatesUniquenessOf","tags":{"categoryClass":"validationfunctions","sectionClass":"modelconfiguration","category":"Validation Functions","section":"Model Configuration"}},{"extended":{"hasExtended":true,"docs":"
// Create a new employee object\nemployee = model("employee").new();\n\n1. Assume 'firstName' is a varchar(50) column\nemployee.validationTypeForProperty("firstName") (This will output: "string")\n\n2. Assume 'hireDate' is a datetime column\nemployee.validationTypeForProperty("hireDate") (This will output: "date")\n\n3. Assume 'salary' is a numeric column\nemployee.validationTypeForProperty("salary") (This will output: "numeric")\n
"},"hint":"Returns the type of validation that Wheels would apply for a given property. This is useful if you want to dynamically inspect a model's property type or apply logic based on the property's expected format.\n\n","returntype":"any","slug":"model.validationTypeForProperty","parameters":[{"required":true,"hint":"Name of column to retrieve data for.","name":"property","type":"string"}],"availableIn":["model"],"name":"validationTypeForProperty","tags":{"categoryClass":"miscellaneousfunctions","sectionClass":"modelclass","category":"Miscellaneous Functions","section":"Model Class"}},{"extended":{"hasExtended":true,"docs":"
1. Get verification chain, remove the first item, and set it back.\nmyVerificationChain = verificationChain();\nArrayDeleteAt(myVerificationChain, 1);\nsetVerificationChain(myVerificationChain);\n
"},"hint":"Returns an array of all verifications (filters, before-actions, or checks) that are configured for the current controller, in the order they will be executed. This allows you to inspect, modify, or reorder the verifications dynamically.\n\n","returntype":"array","slug":"controller.verificationChain","parameters":[],"availableIn":["controller"],"name":"verificationChain","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
1. Tell Wheels to verify that the `handleForm` action is always a `POST` request when executed.\nverifies(only="handleForm", post=true);\n\n2. Make sure that the edit action is a `GET` request, that `userId` exists in the `params` struct, and that it's an integer.\nverifies(only="edit", get=true, params="userId", paramsTypes="integer");\n\n3. Just like above, only this time we want to invoke a custom function in our controller to handle the request when it is invalid.\nverifies(only="edit", get=true, params="userId", paramsTypes="integer", handler="myCustomFunction");\n\n4. Just like above, only this time instead of specifying a handler, we want to `redirect` the visitor to the index action of the controller and show an error in The Flash when the request is invalid.\nverifies(only="edit", get=true, params="userId", paramsTypes="integer", action="index", error="Invalid userId");\n
"},"hint":"Instructs a Wheels controller to check that certain criteria are met before executing an action. This is useful for enforcing request types, required parameters, session/cookie values, or custom verifications. Note that all undeclared arguments will be passed to redirectTo() call if a handler is not specified.\n\n","returntype":"void","slug":"controller.verifies","parameters":[{"default":"","required":false,"hint":"List of action names to limit this verification to.","name":"only","type":"string"},{"default":"","required":false,"hint":"List of action names to exclude this verification from.","name":"except","type":"string"},{"default":"","required":false,"hint":"Set to true to verify that this is a `POST` request.","name":"post","type":"any"},{"default":"","required":false,"hint":"Set to true to verify that this is a `GET` request.","name":"get","type":"any"},{"default":"","required":false,"hint":"Set to true to verify that this is an `AJAX` request.","name":"ajax","type":"any"},{"default":"","required":false,"hint":"Verify that the passed in variable name exists in the cookie scope.","name":"cookie","type":"string"},{"default":"","required":false,"hint":"Verify that the passed in variable name exists in the session scope.","name":"session","type":"string"},{"default":"","required":false,"hint":"Verify that the passed in variable name exists in the params struct.","name":"params","type":"string"},{"default":"","required":false,"hint":"Pass in the name of a function that should handle failed verifications. The default is to just abort the request when a verification fails.","name":"handler","type":"string"},{"default":"","required":false,"hint":"List of types to check each listed cookie value against (will be passed through to your CFML engine's `IsValid` function).","name":"cookieTypes","type":"string"},{"default":"","required":false,"hint":"List of types to check each list session value against (will be passed through to your CFML engine's `IsValid` function).","name":"sessionTypes","type":"string"},{"default":"","required":false,"hint":"List of types to check each params value against (will be passed through to your CFML engine's `IsValid` function).","name":"paramsTypes","type":"string"}],"availableIn":["controller"],"name":"verifies","tags":{"categoryClass":"configurationfunctions","sectionClass":"controller","category":"Configuration Functions","section":"Controller"}},{"extended":{"hasExtended":true,"docs":"
<cfscript>\n\nmapper()\n    // Enables `[controller]` and `[controller]/[action]`, only via `GET` requests.\n    .wildcard()\n\n    // Enables `[controller]/[action]/[key]` as well.\n    .wildcard(mapKey=true)\n\n    // Also enables patterns like `[controller].[format]` and\n    // `[controller]/[action].[format]`\n    .wildcard(mapFormat=true)\n\n    // Allow additional methods beyond just `GET`\n    //\n    // Note that this can open some serious security holes unless you use `verifies`\n    // in the controller to make sure that requests changing data can only occur\n    // with a `POST` method.\n    .wildcard(methods="get,post")\n.end();\n\n</cfscript>
"},"hint":"Automatically generates dynamic routes for your controllers using placeholders like [controller], [action], and optionally [key] or [format]. This allows you to quickly map standard URL patterns to controllers and actions without explicitly defining every route.\n","returntype":"struct","slug":"mapper.wildcard","parameters":[{"default":"get","required":false,"hint":"List of HTTP methods (verbs) to generate the wildcard routes for. We strongly recommend leaving the default value of `get` and using other routing mappers if you need to `POST` to a URL endpoint. For better readability, you can also pass this argument as `methods` if you're listing multiple methods.","name":"method","type":"string"},{"default":"index","required":false,"hint":"Default action to specify if the value for the `[action]` placeholder is not provided.","name":"action","type":"string"},{"default":false,"required":false,"hint":"Whether or not to enable a `[key]` matcher, enabling a `[controller]/[action]/[key]` pattern.","name":"mapKey","type":"boolean"},{"default":false,"required":false,"hint":"Whether or not to add an optional `.[format]` pattern to the end of the generated routes. This is useful for providing formats via URL like `json`, `xml`, `pdf`, etc.","name":"mapFormat","type":"boolean"}],"availableIn":["mapper"],"name":"wildcard","tags":{"categoryClass":"routing","sectionClass":"configuration","category":"Routing","section":"Configuration"}},{"extended":{"hasExtended":true,"docs":"
1. Basic truncation (default truncate string "...")\nwordTruncate(text="Wheels is a framework for ColdFusion", length=4)\n// Output:\n// Wheels is a framework...\n\n2. Truncate with a custom string\nwordTruncate(text="Wheels is a framework for ColdFusion", length=3, truncateString=" (more)")\n// Output:\n// Wheels is a (more)\n\n3. Using with shorter text than length (no truncation applied)\nwordTruncate(text="Hello world", length=5)\n// Output:\n// Hello world\n\n4. Dynamic usage in a view\n<cfoutput>\n    wordTruncate(text=post.content, length=10)\n</cfoutput>\n// Useful for showing previews of long content while preserving word boundaries.\n
"},"hint":"Truncates text to the specified length of words and replaces the remaining characters with the specified truncate string (which defaults to \"...\").\n\n","returntype":"string","slug":"controller.wordTruncate","parameters":[{"required":true,"hint":"The text to truncate.","name":"text","type":"string"},{"default":5,"required":false,"hint":"Number of words to truncate the text to.","name":"length","type":"numeric"},{"default":"...","required":false,"hint":"String to replace the last characters with.","name":"truncateString","type":"string"}],"availableIn":["controller","model","test","migrator","migration","tabledefinition"],"name":"wordTruncate","tags":{"categoryClass":"stringfunctions","sectionClass":"globalhelpers","category":"String Functions","section":"Global Helpers"}},{"extended":{"hasExtended":true,"docs":"
1. Basic year dropdown\nyearSelectTag(name="yearOfBirthday", selected=params.yearOfBirthday)\n\n2. Custom range (past 50 years, at least 18 years ago)\nfiftyYearsAgo = Year(Now()) - 50;\neighteenYearsAgo = Year(Now()) - 18;\n\nyearSelectTag(\n    name="yearOfBirthday",\n    selected=params.yearOfBirthday,\n    startYear=fiftyYearsAgo,\n    endYear=eighteenYearsAgo\n)\n\n3. Include a blank option\nyearSelectTag(name="graduationYear", includeBlank="- Select Year -")\n\n4. Add label with custom placement\nyearSelectTag(\n    name="yearOfHiring",\n    label="Hiring Year",\n    labelPlacement="aroundRight"\n)\n
"},"hint":"Builds and returns a string containing a select form control for a range of years based on the supplied name.\n\n","returntype":"string","slug":"controller.yearSelectTag","parameters":[{"required":true,"hint":"Name to populate in tag's name attribute.","name":"name","type":"string"},{"default":"","required":false,"hint":"The year that should be selected initially.","name":"selected","type":"string"},{"default":2018,"required":false,"hint":"First year in `select` list.","name":"startYear","type":"numeric"},{"default":2028,"required":false,"hint":"Last year in `select` list.","name":"endYear","type":"numeric"},{"default":false,"required":false,"hint":"Whether to include a blank option in the select form control. Pass true to include a blank line or a string that should represent what display text should appear for the empty value (for example, \"- Select One -\").","name":"includeBlank","type":"any"},{"default":"","required":false,"hint":"The label text to use in the form control.","name":"label","type":"string"},{"default":"around","required":false,"hint":"Whether to place the label before, after, or wrapped around the form control. Label text placement can be controlled using aroundLeft or aroundRight.","name":"labelPlacement","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control. Useful to wrap the form control with HTML tags.","name":"prepend","type":"string"},{"default":"","required":false,"hint":"String to append to the form control. Useful to wrap the form control with HTML tags.","name":"append","type":"string"},{"default":"","required":false,"hint":"String to prepend to the form control's label. Useful to wrap the form control with HTML tags.","name":"prependToLabel","type":"string"},{"default":"","required":false,"hint":"String to append to the form control's label. Useful to wrap the form control with HTML tags.","name":"appendToLabel","type":"string"},{"default":true,"required":false,"hint":"Use this argument to decide whether the output of the function should be encoded in order to prevent Cross Site Scripting (XSS) attacks. Set it to `true` to encode all relevant output for the specific HTML element in question (e.g. tag content, attribute values, and URLs). For HTML elements that have both tag content and attribute values you can set this argument to `attributes` to only encode attribute values and not tag content.","name":"encode","type":"any"},{"default":"[runtime expression]","required":false,"name":"$now","type":"date"}],"availableIn":["controller"],"name":"yearSelectTag","tags":{"categoryClass":"formtagfunctions","sectionClass":"viewhelpers","category":"Form Tag Functions","section":"View Helpers"}}]} \ No newline at end of file From e886d0099c2c4b208166a23929e1113fae52a501 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Tue, 27 Jan 2026 15:49:54 +0500 Subject: [PATCH 020/405] commit: update wheels reload command, checks for password --- cli/src/commands/wheels/reload.cfc | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/cli/src/commands/wheels/reload.cfc b/cli/src/commands/wheels/reload.cfc index abe432c898..89dfaefa05 100644 --- a/cli/src/commands/wheels/reload.cfc +++ b/cli/src/commands/wheels/reload.cfc @@ -24,11 +24,99 @@ component aliases='wheels r' extends="base" { requireWheelsApp(getCWD()); arguments=reconstructArgs(arguments); var serverDetails = $getServerInfo(); + var appSettings = $getAppSettings(mode); + var reloadPassword = StructKeyExists(appSettings, "reloadPassword") ? appSettings.reloadPassword : ""; + getURL = serverDetails.serverURL & "/index.cfm?reload=#mode#"; + + // Handle password logic + if (len(reloadPassword)) { + // Password is configured + if (len(password)) { + // User provided a password, validate it against configured one + if (password != reloadPassword) { + detailOutput.error("Invalid password. The configured reload password does not match the provided password."); + return; + } + getURL &= "&password=#password#"; + } else { + detailOutput.error("Reload password is configured but not provided!"); + } + } else { + // No password configured - check if user provided one unnecessarily + if (len(password)) { + detailOutput.statusWarning("No reload password is configured in settings, but you provided one. Proceeding without password."); + } + } getURL = serverDetails.serverURL & "/index.cfm?reload=#mode#&password=#password#"; var loc = new Http( url=getURL ).send().getPrefix(); detailOutput.statusSuccess("Reload Request sent"); } + private struct function $getAppSettings(required string mode="development") { + try { + local.appPath = getCWD(); + local.settingsFile = local.appPath & "/config/settings.cfm"; + local.envSettingsFile = local.appPath & "/config/" & arguments.mode & "/settings.cfm"; + local.settings = {}; + + // Override with app settings if file exists + if (FileExists(local.settingsFile)) { + local.settingsContent = FileRead(local.settingsFile); + parseSettings(local.settingsContent, local.settings); + } + + // Override with environment-specific settings + if (FileExists(local.envSettingsFile)) { + local.envSettingsContent = FileRead(local.envSettingsFile); + parseSettings(local.envSettingsContent, local.settings); + } + + return local.settings; + } catch (any e) { + detailOutput.error("Error reading settings: #e.message#"); + if (StructKeyExists(e, "detail") && Len(e.detail)) { + detailOutput.output("Details: #e.detail#"); + } + } + } + + private void function parseSettings(required string content, required struct settings) { + local.pattern = '(?i)set\s*\(\s*([^)]+)\)'; + local.matches = REMatch(local.pattern, arguments.content); + + for (local.match in local.matches) { + try { + // extract the inside of set(...) + local.inner = REReplace(local.match, '(?i)^set\s*\(|\);?$', '', 'all'); + + // split only on FIRST = + local.eqPos = Find("=", local.inner); + if (!local.eqPos) continue; + + local.key = Trim(Left(local.inner, local.eqPos - 1)); + local.value = Trim(Mid(local.inner, local.eqPos + 1)); + + // strip quotes + local.value = REReplace(local.value, "^['""]|['""]$", "", "all"); + + // coerce types + if (local.value == "true") { + local.value = true; + } else if (local.value == "false") { + local.value = false; + } else if (IsNumeric(local.value)) { + local.value = Val(local.value); + } + + arguments.settings[local.key] = local.value; + } catch (any e) { + detailOutput.error("Error reading settings: #e.message#"); + if (StructKeyExists(e, "detail") && Len(e.detail)) { + detailOutput.output("Details: #e.detail#"); + } + } + } + } } From 7b49f72eb33d672a476cccb78c7478913f19f16f Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Tue, 27 Jan 2026 15:51:22 +0500 Subject: [PATCH 021/405] add allowExplicitTimestamps arguments to save function --- core/src/wheels/model/create.cfc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/wheels/model/create.cfc b/core/src/wheels/model/create.cfc index 3be80d9307..ff66b70ce0 100644 --- a/core/src/wheels/model/create.cfc +++ b/core/src/wheels/model/create.cfc @@ -35,7 +35,8 @@ component { parameterize = arguments.parameterize, reload = arguments.reload, transaction = arguments.transaction, - validate = arguments.validate + validate = arguments.validate, + allowExplicitTimestamps = arguments.allowExplicitTimestamps ); return local.rv; } From c39cc96081204ece94221204894ef28089db1d6c Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Tue, 27 Jan 2026 15:52:19 +0500 Subject: [PATCH 022/405] commit: added return to properly exit the function. --- cli/src/commands/wheels/reload.cfc | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/commands/wheels/reload.cfc b/cli/src/commands/wheels/reload.cfc index 89dfaefa05..df15aac36d 100644 --- a/cli/src/commands/wheels/reload.cfc +++ b/cli/src/commands/wheels/reload.cfc @@ -41,6 +41,7 @@ component aliases='wheels r' extends="base" { getURL &= "&password=#password#"; } else { detailOutput.error("Reload password is configured but not provided!"); + return; } } else { // No password configured - check if user provided one unnecessarily From 6e1e35d8e6624728faf23c1d5286a0b300bce6b0 Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Tue, 27 Jan 2026 17:05:49 +0500 Subject: [PATCH 023/405] Remove allowExplicitTimestamps from filterList --- core/src/wheels/model/create.cfc | 13 +++++++------ core/src/wheels/model/update.cfc | 7 +++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/wheels/model/create.cfc b/core/src/wheels/model/create.cfc index ff66b70ce0..5c6460a743 100644 --- a/core/src/wheels/model/create.cfc +++ b/core/src/wheels/model/create.cfc @@ -27,7 +27,7 @@ component { $args(name = "create", args = arguments); $setProperties( argumentCollection = arguments, - filterList = "properties,parameterize,reload,validate,transaction,callbacks,allowExplicitTimestamps" + filterList = "properties,parameterize,reload,validate,transaction,callbacks" ); local.rv = new (argumentCollection = arguments); local.rv.save( @@ -35,8 +35,7 @@ component { parameterize = arguments.parameterize, reload = arguments.reload, transaction = arguments.transaction, - validate = arguments.validate, - allowExplicitTimestamps = arguments.allowExplicitTimestamps + validate = arguments.validate ); return local.rv; } @@ -173,7 +172,8 @@ component { if (!Len(key())) { local.rollback = true; } - $create(parameterize = arguments.parameterize, reload = arguments.reload, allowExplicitTimestamps = arguments.allowExplicitTimestamps?:false); + + $create(parameterize = arguments.parameterize, reload = arguments.reload); if ( $saveAssociations(argumentCollection = arguments) && $callback("afterCreate", arguments.callbacks) @@ -199,7 +199,7 @@ component { && $callback("beforeSave", arguments.callbacks) && $callback("beforeUpdate", arguments.callbacks) ) { - $update(parameterize = arguments.parameterize, reload = arguments.reload, allowExplicitTimestamps = arguments.allowExplicitTimestamps?:false); + $update(parameterize = arguments.parameterize, reload = arguments.reload); if ( $saveAssociations(argumentCollection = arguments) && $callback("afterUpdate", arguments.callbacks) @@ -226,7 +226,8 @@ component { */ public boolean function $create(required any parameterize, required boolean reload) { // Allow explicit assignment of the createdAt/updatedAt properties if allowExplicitTimestamps is true - local.allowExplicitTimestamps = StructKeyExists(arguments, "allowExplicitTimestamps") && arguments.allowExplicitTimestamps; + local.allowExplicitTimestamps = StructKeyExists(this, "allowExplicitTimestamps") && this.allowExplicitTimestamps; + if ( local.allowExplicitTimestamps && StructKeyExists(this, $get("timeStampOnCreateProperty")) diff --git a/core/src/wheels/model/update.cfc b/core/src/wheels/model/update.cfc index 8a228ed478..d4961c9d4a 100644 --- a/core/src/wheels/model/update.cfc +++ b/core/src/wheels/model/update.cfc @@ -244,15 +244,14 @@ component { $args(name = "update", args = arguments); $setProperties( argumentCollection = arguments, - filterList = "properties,parameterize,reload,validate,transaction,callbacks,allowExplicitTimestamps" + filterList = "properties,parameterize,reload,validate,transaction,callbacks" ); return save( callbacks = arguments.callbacks, parameterize = arguments.parameterize, reload = arguments.reload, transaction = arguments.transaction, - validate = arguments.validate, - allowExplicitTimestamps = arguments.allowExplicitTimestamps + validate = arguments.validate ); } @@ -304,7 +303,7 @@ component { // Perform update if changes have been made. if (hasChanged()) { // Allow explicit assignment of the createdAt/updatedAt properties if allowExplicitTimestamps is true - local.allowExplicitTimestamps = StructKeyExists(arguments, "allowExplicitTimestamps") && arguments.allowExplicitTimestamps; + local.allowExplicitTimestamps = StructKeyExists(this, "allowExplicitTimestamps") && this.allowExplicitTimestamps; if ( local.allowExplicitTimestamps && StructKeyExists(this, $get("timeStampOnUpdateProperty")) From 12fe9d34300b72b3166d730a0218ae7d30684ccc Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Tue, 27 Jan 2026 19:08:26 +0500 Subject: [PATCH 024/405] commit: update wheels cli docs - updating outputs of commands --- cli/src/commands/wheels/init.cfc | 4 +- .../commands/analysis/analyze-performance.md | 2 +- .../commands/application-utilities/about.md | 11 +- .../commands/config/config-diff.md | 113 ++++++------ .../command-line-tools/commands/core/deps.md | 16 +- .../commands/core/destroy.md | 6 +- .../command-line-tools/commands/core/init.md | 8 +- .../commands/database/db-create.md | 49 ++--- .../commands/database/db-drop.md | 16 +- .../commands/database/dbmigrate-info.md | 66 +++++-- .../commands/database/dbmigrate-latest.md | 86 +++++++-- .../commands/docs/docs-generate.md | 43 ++--- .../commands/docs/docs-serve.md | 18 +- .../commands/environment/env-list.md | 35 ++-- .../commands/environment/env-show.md | 120 +++++++------ .../commands/environment/env-switch.md | 24 +-- .../commands/environment/env-validate.md | 73 ++++---- .../commands/generate/snippets.md | 4 +- .../commands/get/get-environment.md | 14 +- .../commands/get/get-settings.md | 72 ++++++-- .../commands/plugins/plugins-info.md | 74 +++++--- .../commands/plugins/plugins-init.md | 38 ++-- .../commands/plugins/plugins-install.md | 29 +-- .../commands/plugins/plugins-list.md | 43 +++-- .../commands/plugins/plugins-remove.md | 12 +- .../commands/plugins/plugins-search.md | 167 +++++++++++++----- .../commands/plugins/plugins-update.md | 21 +-- .../commands/security/security-scan.md | 8 + 28 files changed, 749 insertions(+), 423 deletions(-) diff --git a/cli/src/commands/wheels/init.cfc b/cli/src/commands/wheels/init.cfc index b51dbb6503..b344ed559a 100644 --- a/cli/src/commands/wheels/init.cfc +++ b/cli/src/commands/wheels/init.cfc @@ -64,7 +64,7 @@ component extends="base" { // Create a server.json if one doesn't exist if(!fileExists(serverJsonLocation)){ - var appName = ask( message = "Please enter an application name: we use this to make the server.json servername unique: ", defaultResponse = 'myapp'); + var appName = ask( message = "Please enter an application name (we use this to make the server.json servername unique): ", defaultResponse = 'myapp'); appName = helpers.stripSpecialChars(appName); var setEngine = ask( message = 'Please enter a default cfengine: ', defaultResponse = 'lucee@6' ); @@ -84,7 +84,7 @@ component extends="base" { // Create a box.json if one doesn't exist if(!fileExists(boxJsonLocation)){ if(!isDefined("appName")) { - var appName = ask("Please enter an application name: we use this to make the box.json servername unique: "); + var appName = ask("Please enter an application name (we use this to make the box.json servername unique): "); appName = helpers.stripSpecialChars(appName); } var boxJSON = fileRead( getTemplate('/BoxJSON.txt' ) ); diff --git a/docs/src/command-line-tools/commands/analysis/analyze-performance.md b/docs/src/command-line-tools/commands/analysis/analyze-performance.md index ba4a256356..afb1102d2c 100644 --- a/docs/src/command-line-tools/commands/analysis/analyze-performance.md +++ b/docs/src/command-line-tools/commands/analysis/analyze-performance.md @@ -101,7 +101,7 @@ Target: all Threshold: 100ms [====================] 100% Complete! - +Profiling mode disabled ================================================== PERFORMANCE ANALYSIS COMPLETE ================================================== diff --git a/docs/src/command-line-tools/commands/application-utilities/about.md b/docs/src/command-line-tools/commands/application-utilities/about.md index dac2fe3a31..38cacd987d 100644 --- a/docs/src/command-line-tools/commands/application-utilities/about.md +++ b/docs/src/command-line-tools/commands/application-utilities/about.md @@ -26,12 +26,11 @@ wheels about Output: ``` - _____ _______ ___ _ - / ____| ____\ \ / / | | | -| | | |__ \ \ /\ / /| |__ ___ ___| |___ -| | | __| \ \/ \/ / | '_ \ / _ \/ _ \ / __| -| |____| | \ /\ / | | | | __/ __/ \__ \ - \_____|_| \/ \/ |_| |_|\___|\___|_|___/ + \ \ / / | | | + \ \ /\ / /| |__ ___ ___| |___ + \ \/ \/ / | '_ \ / _ \/ _ \ / __| + \ /\ / | | | | __/ __/ \__ \ + \/ \/ |_| |_|\___|\___|_|___/ Wheels Framework Version: 2.5.0 diff --git a/docs/src/command-line-tools/commands/config/config-diff.md b/docs/src/command-line-tools/commands/config/config-diff.md index 776f373f02..38c4eed0cc 100644 --- a/docs/src/command-line-tools/commands/config/config-diff.md +++ b/docs/src/command-line-tools/commands/config/config-diff.md @@ -109,11 +109,13 @@ project_root/ The table output is organized into clear sections: ``` -======================================== +================================================== Configuration Comparison: development vs production -======================================== +================================================== -[SETTINGS CONFIGURATION] + +ENVIRONMENT VARIABLES +-------------------------------------------------- Different Values: ┌──────────────────────┬────────────┬────────────┐ @@ -147,72 +149,71 @@ Different Values: │ DEBUG_MODE │ true │ false │ └──────────────┴────────────────┴────────────────┘ -======================================== -SUMMARY -======================================== +================================================== + SUMMARY +================================================== Settings: - Total: 25 - Identical: 20 - Different: 2 - Unique: 3 +Total: 25 +Identical: 20 +Different: 2 +Unique: 3 Environment Variables: - Total: 15 - Identical: 10 - Different: 2 - Unique: 3 +Total: 15 +Identical: 10 +Different: 2 +Unique: 3 Overall: - Total configurations: 40 - Identical: 30 - Different: 4 - Unique: 6 - Similarity: 75% +Total configurations: 40 +Identical: 30 +Different: 4 +Unique: 6 +Similarity: 75% ``` ### JSON Format ```json { - "env1": "development", - "env2": "production", - "comparisons": { - "settings": { - "identical": [...], - "different": [...], - "onlyInFirst": [...], - "onlyInSecond": [...] - }, - "env": { - "identical": [...], - "different": [...], - "onlyInFirst": [...], - "onlyInSecond": [...] - } - }, - "summary": { - "settings": { - "totalSettings": 25, - "identical": 20, - "different": 2, - "onlyInFirst": 1, - "onlyInSecond": 2 - }, - "env": { - "totalVariables": 15, - "identical": 10, - "different": 2, - "onlyInFirst": 1, - "onlyInSecond": 2 + "ENV1": "development", + "ENV2": "production", + "COMPARISONS":{ + "SETTINGS":{ + "DIFFERENT":[...], + "ONLYINSECOND":[...], + "ONLYINFIRST":[...] + }, + "ENV": { + "ONLYINSECOND":[...], + "DIFFERENT":[...], + "IDENTICAL":[...], + "ONLYINFIRST":[...] + } }, - "overall": { - "total": 40, - "identical": 30, - "different": 4, - "unique": 6, - "similarity": 75 + "SUMMARY": { + "ENV":{ + "TOTALVARIABLES":12, + "ONLYINSECOND":1, + "DIFFERENT":0, + "IDENTICAL":0, + "ONLYINFIRST":11 + }, + "OVERALL":{ + "UNIQUE":12, + "SIMILARITY":14, + "DIFFERENT":0, + "TOTAL":14, + "IDENTICAL":2 + }, + "SETTINGS":{ + "ONLYINSECOND":0, + "TOTALSETTINGS":2, + "DIFFERENT":0, + "IDENTICAL":2, + "ONLYINFIRST":0 + } } - } } ``` diff --git a/docs/src/command-line-tools/commands/core/deps.md b/docs/src/command-line-tools/commands/core/deps.md index c3968ddb4f..d1c83017eb 100644 --- a/docs/src/command-line-tools/commands/core/deps.md +++ b/docs/src/command-line-tools/commands/core/deps.md @@ -126,18 +126,27 @@ The report includes: Example output: ``` -Dependency Report: +================================================== + Wheels Dependency Manager +================================================== + +Generating dependency report... +================================================== + Dependency Report +================================================== Generated: 2025-09-19 11:38:44 Wheels Version: 3.0.0-SNAPSHOT CFML Engine: Lucee 5.4.6.9 Dependencies: +-------------------------------------------------- cbvalidation @ ^4.6.0+28 - Installed: No shortcodes @ ^0.0.4 - Installed: No wirebox @ ^7.4.2+24 - Installed: No Dev Dependencies: +-------------------------------------------------- testbox @ ^6.4.0+17 - Installed: Yes Checking for outdated packages... @@ -147,9 +156,12 @@ Checking for outdated packages... │ testbox@^6.4.. │ 6.4.0+17 │ 6.4.0+17 │ 6.4.0+17 │ /testbox │ └────────────────┴───────────┴──────────┴──────────┴─────────────────────┘ +Checking for outdated packages... + +Checking for outdated dependencies, please wait... There are no outdated dependencies! -Full report exported to: dependency-report-20250919-113851.json +[SUCCESS]: Full report exported to: dependency-report-20250919-113851.json ``` ## Integration with CommandBox diff --git a/docs/src/command-line-tools/commands/core/destroy.md b/docs/src/command-line-tools/commands/core/destroy.md index 394eaa7223..a2edb03ec1 100644 --- a/docs/src/command-line-tools/commands/core/destroy.md +++ b/docs/src/command-line-tools/commands/core/destroy.md @@ -71,9 +71,9 @@ wheels d user This will prompt this along with a confirmation: ``` -================================================ -= Watch Out! = -================================================ +================================================== + Watch Out! +================================================== This will delete the associated database table 'users', and the following files and directories: diff --git a/docs/src/command-line-tools/commands/core/init.md b/docs/src/command-line-tools/commands/core/init.md index b28ce05922..c3083bb38e 100644 --- a/docs/src/command-line-tools/commands/core/init.md +++ b/docs/src/command-line-tools/commands/core/init.md @@ -38,7 +38,9 @@ wheels init Example interaction: ``` -==================================== Wheels init =================================== +================================================== + Wheels init +================================================== This function will attempt to add a few things to an EXISTING Wheels installation to help the CLI interact. @@ -50,10 +52,10 @@ Example interaction: 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 an application name (we use this to make the server.json servername unique): myapp Please enter a default cfengine: lucee5 ``` diff --git a/docs/src/command-line-tools/commands/database/db-create.md b/docs/src/command-line-tools/commands/database/db-create.md index a577b92561..c992b6d8f9 100644 --- a/docs/src/command-line-tools/commands/database/db-create.md +++ b/docs/src/command-line-tools/commands/database/db-create.md @@ -177,27 +177,27 @@ wheels db create --dbtype=mysql --database=myapp_dev --force **Output Example:** ``` -================================================================== - Database Creation Process -================================================================== +================================================== + Database Creation +================================================== Datasource: myapp_dev Environment: development ------------------------------------------------------------------- +-------------------------------------------------- Database Type: MySQL Database Name: myapp_dev ------------------------------------------------------------------- - ->> Initializing MySQL database creation... - [OK] Driver found: com.mysql.cj.jdbc.Driver - [OK] Connected successfully to MySQL server! - ->> Creating MySQL database 'myapp_dev'... - [OK] Database 'myapp_dev' created successfully! - ->> Verifying database creation... - [OK] Database 'myapp_dev' verified successfully! ------------------------------------------------------------------- - [OK] MySQL database creation completed successfully! +-------------------------------------------------- +Initializing MySQL database creation... +Connecting to MySQL server... +[SUCCESS]: Driver found: com.mysql.cj.jdbc.Driver +[SUCCESS]: Connected successfully to MySQL server! +Checking if database exists... +Creating MySQL database 'myapp_dev'... +[SUCCESS]: Database 'myapp_dev' created successfully! +Verifying database creation... +[SUCCESS]: Database 'myapp_dev' verified successfully! +-------------------------------------------------- +MySQL database creation completed successfully! +Writing datasource to app.cfm... ``` --- @@ -330,10 +330,13 @@ If the specified datasource doesn't exist, the command will prompt you to create Datasource 'myapp_dev' not found in server configuration. Would you like to create this datasource now? [y/n]: y +================================================== + Interactive Datasource Creation +================================================== -=== Interactive Datasource Creation === -Select database type: +Supported Database Types +-------------------------------------------------- 1. MySQL 2. PostgreSQL 3. SQL Server (MSSQL) @@ -342,16 +345,18 @@ Select database type: 6. SQLite Select database type [1-6]: 1 -Selected: MySQL +[SUCCESS]: Selected: MySQL -Enter connection details: +Connection Details +-------------------------------------------------- Host [localhost]: Port [3306]: Database name [wheels_dev]: myapp_dev Username [root]: Password: **** -Review datasource configuration: +Configuration Review +-------------------------------------------------- Datasource Name: myapp_dev Database Type: MySQL Host: localhost diff --git a/docs/src/command-line-tools/commands/database/db-drop.md b/docs/src/command-line-tools/commands/database/db-drop.md index c450318ef1..dd0fe86d4d 100644 --- a/docs/src/command-line-tools/commands/database/db-drop.md +++ b/docs/src/command-line-tools/commands/database/db-drop.md @@ -74,10 +74,18 @@ wheels db drop --datasource=sqlite_app Output: ``` -[WARN] WARNING: This will permanently drop the database! -Are you sure you want to drop the database 'D:\MyApp\db\myapp_dev.db'? Type 'yes' to confirm: yes - -[OK] SQLite database dropped successfully! +================================================== + Database Drop Process +================================================== +[WARNING]: This will permanently drop the database! + +Datasource: db_app +Environment: development +-------------------------------------------------- +Database Type: H2 +Database Name: mydb_app +-------------------------------------------------- +Are you sure you want to drop the database 'mydb_app'? Type 'yes' to confirm: ``` If server is running, it will automatically stop and retry: diff --git a/docs/src/command-line-tools/commands/database/dbmigrate-info.md b/docs/src/command-line-tools/commands/database/dbmigrate-info.md index 7f8906691f..b9ee4a02be 100644 --- a/docs/src/command-line-tools/commands/database/dbmigrate-info.md +++ b/docs/src/command-line-tools/commands/database/dbmigrate-info.md @@ -33,19 +33,59 @@ The command displays: ## Example Output ``` -+-----------------------------------------+-----------------------------------------+ -| Datasource: myApp | Total Migrations: 4 | -| Database Type: H2 | Available Migrations: 4 | -| | Current Version: 0 | -| | Latest Version: 20250812161449 | -+-----------------------------------------+-----------------------------------------+ -+----------+------------------------------------------------------------------------+ -| | 20250812161449_cli__create_reporting_procedures | -| | 20250812161302_cli__blacnk | -| | 20250812161250_cli__name | -| | 20250812154338_cli__0 | -+----------+------------------------------------------------------------------------+ -``` + +Database Information +-------------------------------------------------- +Datasource: dbapp_test +Database Type: MySQL + +Migration Status +-------------------------------------------------- +Total Migrations: 17 +Available Migrations: 14 +Current Version: 20260116163515 +Latest Version: 20260123185445 +-------------------------------------------------- + +Migration Files +-------------------------------------------------- +╔══════════╤══════════════════════════════════════════════════════╗ +║ STATUS │ FILE ║ +╠══════════╪══════════════════════════════════════════════════════╣ +║ │ 20260123185445_cli_create_table_user ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260123183026_cli_remove_table_blog_posts ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260123182948_create_blog_posts_table ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260122173254_cli_create_table_user_roles ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119145114_cli_create_column_parts_feature ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119144642_cli_blank_students ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119112924_cli_create_table_students ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119111943_cli_create_table_books ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116170453_cli_create_table_users ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116170311_cli_create_table_users ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116165727_cli_create_column_user_required_field ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116164432_cli_create_column_product_price ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116164211_cli_create_column_user_bio ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116163907_cli_create_column_user_email ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ migrated │ 20260116163515_cli_blank_create_reporting_procedures ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ migrated │ 20260116160315_cli_remove_table_resources ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ migrated │ 20260116155320_cli_remove_table_users ║ +╚══════════╧══════════════════════════════════════════════════════╝``` ## Migration Files Location diff --git a/docs/src/command-line-tools/commands/database/dbmigrate-latest.md b/docs/src/command-line-tools/commands/database/dbmigrate-latest.md index 692db539fb..4b9d9aeee7 100644 --- a/docs/src/command-line-tools/commands/database/dbmigrate-latest.md +++ b/docs/src/command-line-tools/commands/database/dbmigrate-latest.md @@ -28,18 +28,80 @@ None. ## Example Output ``` -+-----------------------------------------+-----------------------------------------+ -| Datasource: myApp | Total Migrations: 4 | -| Database Type: H2 | Available Migrations: 0 | -| | Current Version: 20250812161449 | -| | Latest Version: 20250812161449 | -+-----------------------------------------+-----------------------------------------+ -+----------+------------------------------------------------------------------------+ -| migrated | 20250812161449_cli__create_reporting_procedures | -| migrated | 20250812161302_cli__example_1 | -| migrated | 20250812161250_cli__example_2 | -| migrated | 20250812154338_cli__example_3 | -+----------+------------------------------------------------------------------------+ +================================================== + Updating Database Schema to Latest Version +================================================== + +Latest Version: 20260123185445 +================================================== + Migration Execution +================================================== + +Target Version: 20260123185445 +-------------------------------------------------- +Sending: http://0.0.0.0:8080/?controller=wheels&action=wheels&view=cli&command=migrateTo&version=20260123185445 +[SUCCESS]: Call to bridge was successful. +[SUCCESS]: Migration completed successfully! + +Sending: http://0.0.0.0:8080/?controller=wheels&action=wheels&view=cli&command=info +[SUCCESS]: Call to bridge was successful. +================================================== + Database Migration Status +================================================== + + +Database Information +-------------------------------------------------- +Datasource: dbapp_test +Database Type: MySQL + +Migration Status +-------------------------------------------------- +Total Migrations: 17 +Available Migrations: 14 +Current Version: 20260116163515 +Latest Version: 20260123185445 +-------------------------------------------------- + +Migration Files +-------------------------------------------------- +╔══════════╤══════════════════════════════════════════════════════╗ +║ STATUS │ FILE ║ +╠══════════╪══════════════════════════════════════════════════════╣ +║ │ 20260123185445_cli_create_table_user ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260123183026_cli_remove_table_blog_posts ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260123182948_create_blog_posts_table ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260122173254_cli_create_table_user_roles ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119145114_cli_create_column_parts_feature ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119144642_cli_blank_students ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119112924_cli_create_table_students ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260119111943_cli_create_table_books ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116170453_cli_create_table_users ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116170311_cli_create_table_users ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116165727_cli_create_column_user_required_field ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116164432_cli_create_column_product_price ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116164211_cli_create_column_user_bio ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ │ 20260116163907_cli_create_column_user_email ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ migrated │ 20260116163515_cli_blank_create_reporting_procedures ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ migrated │ 20260116160315_cli_remove_table_resources ║ +╟──────────┼──────────────────────────────────────────────────────╢ +║ migrated │ 20260116155320_cli_remove_table_users ║ +╚══════════╧══════════════════════════════════════════════════════╝ ``` ## Migration Execution diff --git a/docs/src/command-line-tools/commands/docs/docs-generate.md b/docs/src/command-line-tools/commands/docs/docs-generate.md index e31a39b34b..a5d2b439e4 100644 --- a/docs/src/command-line-tools/commands/docs/docs-generate.md +++ b/docs/src/command-line-tools/commands/docs/docs-generate.md @@ -151,41 +151,30 @@ component extends="Controller" { ## Output Example ``` -Documentation Generator +================================================== + Documentation Generator ================================================== Generating documentation... -Scanning source files... -[OK] Found 1 models -[OK] Found 1 controllers - -Writing documentation... [OK] HTML files generated + create directory public/api-docs -================================================== -[SUCCESS] Documentation generated successfully! +Scanning Source Files +-------------------------------------------------- +[SUCCESS]: Found 1 models +[SUCCESS]: Found 4 controllers -Summary: - - Models: 1 files - - Controllers: 1 files - - Total: 2 components documented - -Output directory: C:\path\to\docs\api\ -``` +Writing documentation... +[SUCCESS]: HTML files generated -## Documentation Features +[SUCCESS]: Documentation generated successfully! -### Auto-generated Content -- Class hierarchies and inheritance -- Method signatures and parameters -- Property types and defaults -- Relationship diagrams -- Route mappings -- Database ERD +Summary +-------------------------------------------------- +Models: 1 files +Controllers: 4 files +Total Components: 5 documented -## Notes +[INFO]: Output directory: C:\Users\Hp\cli_testingapp\db_app\public\api-docs -- Documentation is generated from code comments -- Use consistent JavaDoc format for best results -- Private methods are excluded by default \ No newline at end of file diff --git a/docs/src/command-line-tools/commands/docs/docs-serve.md b/docs/src/command-line-tools/commands/docs/docs-serve.md index 0c6f0aba0a..c9a81429df 100644 --- a/docs/src/command-line-tools/commands/docs/docs-serve.md +++ b/docs/src/command-line-tools/commands/docs/docs-serve.md @@ -50,23 +50,23 @@ wheels docs serve --root=docs/generated --port=3000 ## Server Output ``` -Output directory: D:\Command Box\wheels\templates\base\src\docs\api\ -CommandBox:src> wheels docs serve +================================================== + Documentation Server +================================================== + +Starting documentation server... √ | Starting Server | √ | Setting site [wheels-docs-BBAA12EF-7A83-4D03-BD6DBFE4AC17C1F9] Profile to [development] | √ | Loading CFConfig into server -Status: starting -Server is still starting... waiting... -Server is up and running! -Starting documentation server... +[SUCCESS]: Documentation server started! -Documentation server started! - -Serving: D:\Command Box\wheels\templates\base\src\docs\api\ +Serving directory: D:\Command Box\wheels\templates\base\src\docs\api\ URL: http://localhost:35729 Opening browser... +[SUCCESS]: Server is up and running! + Press Ctrl+C to stop the server ``` diff --git a/docs/src/command-line-tools/commands/environment/env-list.md b/docs/src/command-line-tools/commands/environment/env-list.md index 723f90935b..bffa422518 100644 --- a/docs/src/command-line-tools/commands/environment/env-list.md +++ b/docs/src/command-line-tools/commands/environment/env-list.md @@ -53,25 +53,36 @@ wheels env list --filter=production ### Basic Output ``` -Available Environments -===================== +================================================== + Available Environments +================================================== - NAME TYPE DATABASE STATUS - development * Development wheels_dev OK Valid - testing Testing wheels_test OK Valid - staging Staging wheels_staging OK Valid - production Production wheels_prod OK Valid - qa Custom wheels_qa WARN Invalid -* = Current environment +╔═════════════╤═════════════╤══════════╤════════════╤════════╤═════════╗ +║ Name │ Type │ Database │ Status │ Active │ DB Type ║ +╠═════════════╪═════════════╪══════════╪════════════╪════════╪═════════╣ +║ development │ Development │ mydb_app │ [OK] Valid │ NO │ mysql ║ +╚═════════════╧═════════════╧══════════╧════════════╧════════╧═════════╝ + + +Total Environments: 1 +Current Environment: +[INFO]: * = Currently active environment +[INFO]: Use 'wheels env list --verbose' for detailed information ``` ### Verbose Output ``` -Available Environments -===================== +================================================== + Available Environments +================================================== + +Total Environments: 1 +Current Environment: + development * [Active] +-------------------------------------------------- Type: Development Database: wheels_dev Datasource: wheels_development @@ -81,6 +92,7 @@ development * [Active] Modified: 2024-01-10 14:23:45 testing +-------------------------------------------------- Type: Testing Database: wheels_test Datasource: wheels_testing @@ -90,6 +102,7 @@ testing Modified: 2024-01-08 09:15:22 staging +-------------------------------------------------- Type: Staging Database: wheels_staging Datasource: wheels_staging diff --git a/docs/src/command-line-tools/commands/environment/env-show.md b/docs/src/command-line-tools/commands/environment/env-show.md index a3660572b1..acd3702da9 100644 --- a/docs/src/command-line-tools/commands/environment/env-show.md +++ b/docs/src/command-line-tools/commands/environment/env-show.md @@ -74,43 +74,31 @@ wheels env show --key=API_KEY The table format groups variables by prefix and displays them in an organized, readable way: ``` -Environment Variables Viewer - -Environment Variables from .env: - -╔════════╤══════════════════════════╤═══════════════════════════╗ -║ Source │ Variable │ Value ║ -╠════════╪══════════════════════════╪═══════════════════════════╣ -║ .env │ DB_HOST │ localhost ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ DB_NAME │ myapp ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ DB_PASSWORD │ ******** ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ DB_PORT │ 3306 ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ DB_USER │ wheels ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ API_BASE_URL │ https://api.example.com ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ API_KEY │ ******** ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ API_TIMEOUT │ 30 ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ WHEELS_ENV │ development ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ WHEELS_RELOAD_PASSWORD │ ******** ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ APP_NAME │ My Application ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ DEBUG_MODE │ true ║ -╟────────┼──────────────────────────┼───────────────────────────╢ -║ .env │ PORT │ 3000 ║ -╚════════╧══════════════════════════╧═══════════════════════════╝ - -Tip: Access these in your app with application.env['KEY_NAME'] -Or use them in config files: set(dataSourceName=application.env['DB_NAME']) -Wheels automatically loads .env on application start +================================================== + Environment Variables Viewer +================================================== + + +Environment Variables from .env +-------------------------------------------------- +╔════════════╤═════════════╤════════╗ +║ Variable │ Value │ Source ║ +╠════════════╪═════════════╪════════╣ +║ DB_NAME │ myapp │ .env ║ +╟────────────┼─────────────┼────────╢ +║ DB_PORT │ 3306 │ .env ║ +╟────────────┼─────────────┼────────╢ +║ DB_USER │ root │ .env ║ +╟────────────┼─────────────┼────────╢ +║ wheels_env │ development │ .env ║ +╚════════════╧═════════════╧════════╝ + +Total variables: 4 +[INFO]: Usage tips: + - Access in app: application.env['VARIABLE_NAME'] + - Use in config: set(value=application.env['VARIABLE_NAME']) + - Wheels loads .env automatically on app start + - Update: wheels env set KEY=VALUE ``` ### JSON Format @@ -195,30 +183,50 @@ If the specified `.env` file doesn't exist, you'll see helpful guidance: No .env file found in project root Create a .env file with key=value pairs, for example: - -## Database Configuration -DB_HOST=localhost -DB_PORT=3306 -DB_NAME=myapp -DB_USER=wheels -DB_PASSWORD=secret - -## Application Settings -WHEELS_ENV=development -WHEELS_RELOAD_PASSWORD=mypassword +-------------------------------------------------- + + +[INFO]: Use 'wheels env set KEY=VALUE' to create environment variables +╔══════════════╤══════════════════════════╤═════════════╗ +║ Source │ Variable │ Value ║ +╠══════════════╪══════════════════════════╪═════════════╣ +║ │ # Database Configuration │ ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ .env.example │ DB_HOST │ localhost ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ .env.example │ DB_PORT │ 3306 ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ .env.example │ DB_NAME │ myapp ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ .env.example │ DB_USER │ wheels ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ .env.example │ DB_PASSWORD │ secret ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ │ # Application Settings │ ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ .env.example │ WHEELS_ENV │ development ║ +╟──────────────┼──────────────────────────┼─────────────╢ +║ .env.example │ WHEELS_RELOAD_PASSWORD │ mypassword ║ +╚══════════════╧══════════════════════════╧═════════════╝ ``` ### Key Not Found When requesting a specific key that doesn't exist: ``` -Environment variable 'MISSING_KEY' not found - -Available keys in .env: - - API_KEY - - DB_HOST - - DB_NAME - - DEBUG_MODE - - WHEELS_ENV +[WARNING]: Environment variable 'API_BASE_URL' not found in .env + + +Available Variables in .env +-------------------------------------------------- +╔═══════════════╤═════════════════════╗ +║ Current Value │ Available Variables ║ +╠═══════════════╪═════════════════════╣ +║ myapp │ DB_NAME ║ +╟───────────────┼─────────────────────╢ +║ 3306 │ DB_PORT ║ +╟───────────────┼─────────────────────╢ +║ root │ DB_USER ║ +╚═══════════════╧═════════════════════╝ ``` ## Common Use Cases diff --git a/docs/src/command-line-tools/commands/environment/env-switch.md b/docs/src/command-line-tools/commands/environment/env-switch.md index 6c95d6e624..7b2d92b656 100644 --- a/docs/src/command-line-tools/commands/environment/env-switch.md +++ b/docs/src/command-line-tools/commands/environment/env-switch.md @@ -84,11 +84,12 @@ wheels env switch production --backup --restart --check ## Output Example ``` -Environment Switch +================================================== + Environment Switch ================================================== -Current Environment: development -Target Environment: staging +Current Environment: development +Target Environment: staging Validating target environment... [OK] Creating backup... [OK] @@ -96,16 +97,17 @@ Creating backup... [OK] Switching environment... [OK] Updated environment variable... [OK] -================================================== -[SUCCESS] Environment switched successfully! +[SUCCESS]: Environment switched successfully! + -Environment Details: -- Environment: staging -- Database: wheels_staging -- Debug Mode: Enabled -- Cache: Partial +Environment Details +-------------------------------------------------- +Current Environment: production +Target Environment: staging +Debug Mode: Enabled +Cache: Partial -IMPORTANT: +[INFO]: IMPORTANT - Restart your application server for changes to take effect - Run 'wheels reload' if using Wheels development server - Or use 'wheels env switch staging --restart' next time diff --git a/docs/src/command-line-tools/commands/environment/env-validate.md b/docs/src/command-line-tools/commands/environment/env-validate.md index b07f975ad1..a277807fbb 100644 --- a/docs/src/command-line-tools/commands/environment/env-validate.md +++ b/docs/src/command-line-tools/commands/environment/env-validate.md @@ -121,12 +121,16 @@ Identifies common placeholder values in sensitive variables: ### Successful Validation ``` -Validating: .env +================================================== + Validating: .env +================================================== -Summary: - Total variables: 12 -Validation passed with no issues! +Summary +-------------------------------------------------- +Total variables: 4 + +[SUCCESS]: Validation passed with no issues! ``` ### Validation with Warnings @@ -145,48 +149,55 @@ Validation passed with 2 warnings ### Validation with Errors ``` -Validating: .env.production +================================================== + Validating: .env +================================================== -Errors found: - Line 3: Invalid format (missing '='): DB_HOST localhost - Required key missing: 'API_KEY' - Line 8: Empty key name +[FAILED]: Errors found: + - Required key missing: 'DB_HOST' + - Required key missing: 'API_KEY' -Warnings: - Line 10: Duplicate key: 'DB_PORT' (previous value will be overwritten) -Summary: - Total variables: 6 +Summary +-------------------------------------------------- +Total variables: 4 -Validation failed with 3 errors +[FAILED]: Validation failed with 2 errors ``` ### Verbose Output ``` -Validating: .env +================================================== + Validating: .env +================================================== + +Summary +-------------------------------------------------- +Total variables: 4 -Summary: - Total variables: 10 Environment Variables: +-------------------------------------------------- + +wheels: + - wheels_env = development - DB: - DB_HOST = localhost - DB_NAME = myapp - DB_PASSWORD = ***MASKED*** - DB_PORT = 3306 - DB_USER = admin +DB: + - DB_USER = root + - DB_NAME = myapp + - DB_PORT = 3306 + - API: - API_BASE_URL = https://api.example.com - API_KEY = ***MASKED*** - API_TIMEOUT = 30 +API: + - API_BASE_URL = https://api.example.com + - API_KEY = ***MASKED*** + - API_TIMEOUT = 30 - Other: - APP_NAME = My Application - WHEELS_ENV = development +Other: + - APP_NAME = My Application + - WHEELS_ENV = development -Validation passed with no issues! +[SUCCESS]: Validation passed with no issues! ``` ## Error Types and Solutions diff --git a/docs/src/command-line-tools/commands/generate/snippets.md b/docs/src/command-line-tools/commands/generate/snippets.md index 3b3ba5316a..eebca75821 100644 --- a/docs/src/command-line-tools/commands/generate/snippets.md +++ b/docs/src/command-line-tools/commands/generate/snippets.md @@ -40,7 +40,9 @@ wheels generate snippets --list Output: ``` -Available Snippets +================================================== + Available Snippets +================================================== Authentication: - login-form - Login form with remember me diff --git a/docs/src/command-line-tools/commands/get/get-environment.md b/docs/src/command-line-tools/commands/get/get-environment.md index 6ba961e2ef..0f9f0fa367 100644 --- a/docs/src/command-line-tools/commands/get/get-environment.md +++ b/docs/src/command-line-tools/commands/get/get-environment.md @@ -28,10 +28,16 @@ wheels get environment This will output something like: ``` -Current Environment: -development +================================================== + Current Environment: development +================================================== -Configured in: .env file (WHEELS_ENV) +Configured in: Using default + +To set an environment: + - wheels env set environment_name + - wheels env switch environment_name + - Set WHEELS_ENV in .env file ``` ## How It Works @@ -183,7 +189,7 @@ The command will show an error if: ### Not a Wheels Application ``` -Error: This command must be run from a Wheels application directory +Error: This command must be run from a Wheels application root directory ``` ### Read Error diff --git a/docs/src/command-line-tools/commands/get/get-settings.md b/docs/src/command-line-tools/commands/get/get-settings.md index 1eed3d04b6..94ce205c3f 100644 --- a/docs/src/command-line-tools/commands/get/get-settings.md +++ b/docs/src/command-line-tools/commands/get/get-settings.md @@ -97,21 +97,41 @@ Total settings: 17 wheels get settings cache ``` ``` -Wheels Settings (development environment): - -cacheActions: false -cacheCullInterval: 5 -cacheCullPercentage: 10 -cacheDatabaseSchema: false -cacheFileChecking: false -cacheImages: false -cacheModelConfig: false -cachePages: false -cachePartials: false -cacheQueries: true -cacheRoutes: false - -Total settings: 11 +================================================== + Wheels Settings (development environment) +================================================== + + +╔═══════════════════════╤═══════╗ +║ Setting │ Value ║ +╠═══════════════════════╪═══════╣ +║ cacheActions │ false ║ +╟───────────────────────┼───────╢ +║ cacheControllerConfig │ false ║ +╟───────────────────────┼───────╢ +║ cacheCullInterval │ true ║ +╟───────────────────────┼───────╢ +║ cacheCullPercentage │ true ║ +╟───────────────────────┼───────╢ +║ cacheDatabaseSchema │ false ║ +╟───────────────────────┼───────╢ +║ cacheFileChecking │ false ║ +╟───────────────────────┼───────╢ +║ cacheImages │ false ║ +╟───────────────────────┼───────╢ +║ cacheModelConfig │ false ║ +╟───────────────────────┼───────╢ +║ cachePages │ false ║ +╟───────────────────────┼───────╢ +║ cachePartials │ false ║ +╟───────────────────────┼───────╢ +║ cacheQueries │ false ║ +╟───────────────────────┼───────╢ +║ cacheRoutes │ false ║ +╟───────────────────────┼───────╢ +║ cacheViewConfig │ false ║ +╚═══════════════════════╧═══════╝ +Total settings: 13 ``` ### Single Setting Display @@ -119,11 +139,25 @@ Total settings: 11 wheels get settings dataSourceName ``` ``` -Wheels Settings (production environment): +================================================== + Wheels Settings (development environment) +================================================== + + +╔════════════════╤══════════════╗ +║ Setting │ Value ║ +╠════════════════╪══════════════╣ +║ dataSourceName │ wheelstestdb ║ +╚════════════════╧══════════════╝ + +Total settings: 1 -dataSourceName: production_db +[INFO]: Settings loaded from: + - config/settings.cfm (global defaults) + - config/development/settings.cfm (environment overrides) -Total settings: 1 +[INFO]: Filtered by: 'dataSourceName' + - Showing 1 matching setting(s) ``` ### No Matches Found @@ -131,7 +165,7 @@ Total settings: 1 wheels get settings nonexistent ``` ``` -No settings found matching 'nonexistent' +[WARNING]: No settings found matching 'nonexistent' ``` ## Common Wheels Settings diff --git a/docs/src/command-line-tools/commands/plugins/plugins-info.md b/docs/src/command-line-tools/commands/plugins/plugins-info.md index 8d643364b3..60e0b060db 100644 --- a/docs/src/command-line-tools/commands/plugins/plugins-info.md +++ b/docs/src/command-line-tools/commands/plugins/plugins-info.md @@ -52,7 +52,9 @@ wheels plugins info wheels-core Plugin Information: wheels-core =========================================================== -Status: + +Status +-------------------------------------------------- [OK] Installed locally Wheels Core @@ -71,9 +73,10 @@ Links: Docs: https://wheels.dev/docs Issues: https://github.com/wheels-dev/wheels/issues -Commands: - Update: wheels plugin update wheels-core - Search: wheels plugin search +Commands +-------------------------------------------------- + - Update: wheels plugin update wheels-core + - Search: wheels plugin search ``` ### Check plugin not installed @@ -83,18 +86,30 @@ wheels plugins info wheels-vue-cli **Output:** ``` -=========================================================== - Plugin Information: wheels-vue-cli -=========================================================== +================================================== + Plugin Information: wheels-vue-cli +================================================== + + + +Status +-------------------------------------------------- +[WARNING]: Not installed -Status: - [X] Not installed +[FAILED]: Plugin Not Found -[ForgeBox package information displayed] +The plugin 'wheels-vue-cli' was not found in: + - Local installation (box.json dependencies) + - ForgeBox repository -Commands: - Install: wheels plugin install wheels-vue-cli - Search: wheels plugin search +[INFO]: Possible reasons + - Plugin name may be misspelled + - Plugin may not exist on ForgeBox + - Network connection issues + +[INFO]: Suggestions + - Search for available plugins: wheels plugin list --available + - Verify the correct plugin name ``` ### Plugin not found anywhere @@ -104,27 +119,30 @@ wheels plugins info nonexistent-plugin **Output:** ``` -=========================================================== - Plugin Information: nonexistent-plugin -=========================================================== +================================================== + Plugin Information: nonexistent-plugin +================================================== + + -Status: - [X] Not installed +Status +-------------------------------------------------- +[WARNING]: Not installed -Plugin Not Installed +[FAILED]: Plugin Not Found The plugin 'nonexistent-plugin' was not found in: - Local installation (box.json dependencies) - ForgeBox repository + - Local installation (box.json dependencies) + - ForgeBox repository -Possible reasons: - Plugin name may be misspelled - Plugin may not exist on ForgeBox - Network connection issues +[INFO]: Possible reasons + - Plugin name may be misspelled + - Plugin may not exist on ForgeBox + - Network connection issues -Suggestions: - Search for available plugins: wheels plugin list --available - Verify the correct plugin name +[INFO]: Suggestions + - Search for available plugins: wheels plugin list --available + - Verify the correct plugin name ``` ## How It Works diff --git a/docs/src/command-line-tools/commands/plugins/plugins-init.md b/docs/src/command-line-tools/commands/plugins/plugins-init.md index da8a34ad66..8d834c46b0 100644 --- a/docs/src/command-line-tools/commands/plugins/plugins-init.md +++ b/docs/src/command-line-tools/commands/plugins/plugins-init.md @@ -36,28 +36,26 @@ The `plugin init` command creates a new CFWheels plugin following the standard C ### Basic plugin initialization ```bash -wheels plugin init my-helper +wheels plugin init myHelper ``` **Output:** ``` -=========================================================== - Initializing Wheels Plugin: wheels-my-helper -=========================================================== +================================================== + Initializing Wheels Plugin: wheels-myHelper +================================================== Creating plugin in /plugins/myHelper/... -=========================================================== +[SUCCESS]: Plugin created successfully in /plugins/myHelper/ -[OK] Plugin created successfully in /plugins/myHelper/ +[INFO]: Files Created: + - myHelper.cfc: Main plugin component + - index.cfm: Documentation page + - box.json: Package metadata + - README.md: Project documentation -Files Created: - myHelper.cfc Main plugin component - index.cfm Documentation page - box.json Package metadata - README.md Project documentation - -Next Steps: +[INFO]: Next Steps: 1. Edit myHelper.cfc to add your plugin functions 2. Update index.cfm and README.md with usage examples 3. Test: wheels reload (then call your functions) @@ -101,7 +99,7 @@ plugins/ ### myHelper.cfc (Main Plugin Component) ```cfml -component hint="wheels-my-helper" output="false" mixin="global" { +component hint="wheels-myHelper" output="false" mixin="global" { public function init() { this.version = "1.0.0"; @@ -132,12 +130,12 @@ component hint="wheels-my-helper" output="false" mixin="global" { ### index.cfm (Documentation Page) ```html -

wheels-my-helper

+

wheels-myHelper

Plugin description

Installation

-wheels plugin install wheels-my-helper
+wheels plugin install wheels-myHelper
 

Usage

@@ -152,10 +150,10 @@ result = myHelperExample("test"); ```json { - "name": "wheels-my-helper", + "name": "wheels-myHelper", "version": "1.0.0", "author": "Your Name", - "slug": "wheels-my-helper", + "slug": "wheels-myHelper", "type": "cfwheels-plugins", "keywords": "cfwheels,wheels,plugin", "homepage": "", @@ -169,7 +167,7 @@ result = myHelperExample("test"); ### 1. Initialize Plugin ```bash -wheels plugin init my-helper --author="Your Name" +wheels plugin init myHelper --author="Your Name" ``` ### 2. Add Your Functions @@ -227,7 +225,7 @@ cd plugins/myHelper git init git add . git commit -m "Initial commit" -git remote add origin https://github.com/username/wheels-my-helper.git +git remote add origin https://github.com/username/wheels-myHelper.git git push -u origin main box login diff --git a/docs/src/command-line-tools/commands/plugins/plugins-install.md b/docs/src/command-line-tools/commands/plugins/plugins-install.md index 2c2f6d0c10..633a7c3916 100644 --- a/docs/src/command-line-tools/commands/plugins/plugins-install.md +++ b/docs/src/command-line-tools/commands/plugins/plugins-install.md @@ -64,24 +64,29 @@ wheels plugins install cfwheels-bcrypt **Output:** ``` -=========================================================== - Installing Plugin -=========================================================== +================================================== + Installing Plugin +================================================== -Plugin: cfwheels-bcrypt -Version: latest -[CommandBox installation output...] +Plugin: cfwheels-bcrypt +Version: latest -=========================================================== +Creating C:\Users\Hp\cli_testingapp\db_app\plugins\/api-tools-1.0.0.zip + ------------------------------------------------------------ +Creating C:\Users\Hp\cli_testingapp\db_app\plugins\/my-helper-1.0.0.zip + √ | Installing package [forgebox:cfwheels-bcrypt] +============================================================ -[OK] Plugin installed successfully! +[SUCCESS]: Plugin installed successfully! -Bcrypt encryption support for Wheels +CFWheels 2.x plugin helper methods for the bCrypt Java Lib -Commands: - wheels plugin list View all installed plugins - wheels plugin info cfwheels-bcrypt View plugin details + +Commands +-------------------------------------------------- + - wheels plugin list View all installed plugins + - wheels plugin info cfwheels-bcrypt View plugin details ``` ### Install specific version diff --git a/docs/src/command-line-tools/commands/plugins/plugins-list.md b/docs/src/command-line-tools/commands/plugins/plugins-list.md index 647f866cbb..ec1718c489 100644 --- a/docs/src/command-line-tools/commands/plugins/plugins-list.md +++ b/docs/src/command-line-tools/commands/plugins/plugins-list.md @@ -40,24 +40,37 @@ wheels plugins list **Output:** ``` -=========================================================== - Installed Wheels Plugins (3) -=========================================================== +================================================== + Installed Wheels Plugins (3) +================================================== + -Plugin Name Version Description ---------------------------------------------------------------- -bcrypt 0.0.4 Bcrypt encryption for Wheels -shortcodes 0.0.4 Shortcode support -wheels-test 1.0.0 Testing utilities +╔══════════════════╤═════════╤════════════════════════════════════════════════════╗ +║ Plugin Name │ Version │ Description ║ +╠══════════════════╪═════════╪════════════════════════════════════════════════════╣ +║ wheels-api-tools │ 1.0.0 │ ║ +╟──────────────────┼─────────┼────────────────────────────────────────────────────╢ +║ CFWheels bCrypt │ 1.0.2 │ CFWheels 2.x plugin helper methods for the bCrypt ║ +╟──────────────────┼─────────┼────────────────────────────────────────────────────╢ +║ wheels-my-helper │ 1.0.0 │ ║ +╚══════════════════╧═════════╧════════════════════════════════════════════════════╝ ------------------------------------------------------------ +------------------------------------------------------------ -[OK] 3 plugins installed +Total plugins: 3 +Latest plugin: wheels-api-tools (1.0.0) -Commands: - wheels plugin info View plugin details - wheels plugin update:all Update all plugins - wheels plugin outdated Check for updates + +Commands +-------------------------------------------------- + - wheels plugin info View plugin details + - wheels plugin update:all Update all plugins + - wheels plugin outdated Check for updates + - wheels plugin install Install new plugin + - wheels plugin remove Remove a plugin + +[INFO]: Tip + Add --format=json for JSON output ``` ### List with no plugins installed @@ -120,6 +133,8 @@ wheels plugins list --available Available Wheels Plugins on ForgeBox =========================================================== +Contacting ForgeBox, please wait... + [Lists all cfwheels-plugins type packages from ForgeBox using 'forgebox show'] ``` diff --git a/docs/src/command-line-tools/commands/plugins/plugins-remove.md b/docs/src/command-line-tools/commands/plugins/plugins-remove.md index f38ac9027d..e62a280af3 100644 --- a/docs/src/command-line-tools/commands/plugins/plugins-remove.md +++ b/docs/src/command-line-tools/commands/plugins/plugins-remove.md @@ -70,18 +70,18 @@ wheels plugins remove wheels-testing --force ### With confirmation prompt (default) ``` Are you sure you want to remove the plugin 'wheels-vue-cli'? (y/n): y -[*] Removing plugin: wheels-vue-cli... +Removing plugin: wheels-vue-cli... +[SUCCESS]: Plugin removed successfully -[OK] Plugin removed successfully -Run 'wheels plugins list' to see remaining plugins +[INFO]: Run 'wheels plugins list' to see remaining plugins ``` ### With force flag ``` [*] Removing plugin: wheels-vue-cli... +[SUCCESS]: Plugin removed successfully -[OK] Plugin removed successfully -Run 'wheels plugins list' to see remaining plugins +[INFO]: Run 'wheels plugins list' to see remaining plugins ``` ### Plugin not installed @@ -89,7 +89,7 @@ Run 'wheels plugins list' to see remaining plugins Are you sure you want to remove the plugin 'bcrypt'? (y/n): y [*] Removing plugin: bcrypt... -[ERROR] Failed to remove plugin: Plugin 'bcrypt' is not installed +[FAILED]: Failed to remove plugin: Plugin 'wheels-vue-cli' is not installed in plugins folder ``` ### Cancellation diff --git a/docs/src/command-line-tools/commands/plugins/plugins-search.md b/docs/src/command-line-tools/commands/plugins/plugins-search.md index b141710071..9312835fa9 100644 --- a/docs/src/command-line-tools/commands/plugins/plugins-search.md +++ b/docs/src/command-line-tools/commands/plugins/plugins-search.md @@ -60,26 +60,88 @@ wheels plugin search **Output:** ``` -=========================================================== - Searching ForgeBox for Wheels Plugins -=========================================================== - -Found 5 plugins: - -Name Version Downloads Description -------------------------------------------------------------------------------- -cfwheels-bcrypt 1.0.2 4393 CFWheels 2.x plugin helper meth... -shortcodes 0.0.4 189 Shortcodes Plugin for CFWheels -cfwheels-authenticateThis 2.0.0 523 Adds bCrypt authentication helpe... -cfwheels-jwt 2.1.0 412 CFWheels plugin for encoding and... -cfwheels-htmx-plugin 1.0.0 678 HTMX Plugin for CFWheels - ------------------------------------------------------------ - -Commands: - wheels plugin install Install a plugin - wheels plugin info View plugin details -``` +================================================== + Searching ForgeBox for Wheels Plugins +================================================== + + +Searching, please wait... + + + +Found 25 plugin(s) +-------------------------------------------------- + +╔═══════════════════════════════════════╤══════════════════════════════════════╤═════════╤═══════════╤════════════════════════════════════════════════════╗ +║ Name │ Slug │ Version │ Downloads │ Description ║ +╠═══════════════════════════════════════╪══════════════════════════════════════╪═════════╪═══════════╪════════════════════════════════════════════════════╣ +║ CFWheels Models Default Scope │ defaultScope │ 0.0.20 │ 4,804 │ CFWheels 2.1+ Add default scope to models for F... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels bCrypt │ cfwheels-bcrypt │ 1.0.2 │ 4,788 │ CFWheels 2.x plugin helper methods for the bCry... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels FlashMessages Bootstrap │ cfwheels-flashmessages-bootstrap │ 1.0.4 │ 3,965 │ CFWheels 2.0 plugin to add Bootstrap tags to fl... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels JS Confirm │ cfwheels-js-confirm │ 1.0.5 │ 3,916 │ JS Confirm - CFWheels Plugin ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ Datepicker │ datepicker │ 2.0.6 │ 3,906 │ Datepicker Plugin for CFWheels ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels Example Plugin │ cfwheels-plugin-example │ 0.0.4 │ 3,804 │ CFWheels Example Plugin ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ Shortcodes │ shortcodes │ 0.0.4 │ 3,700 │ Shortcodes Plugin for CFWheels ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ Bens Json Serializer For Wheels │ cfwheels-bens-json-serializer │ 0.1.7 │ 3,683 │ Swaps renderWith()'s use of serializeJson() wit... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ UnitTest Fixtures │ fixtures │ 0.0.9 │ 3,628 │ Use JSON files to lazily initialize (Create and... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels JWT │ cfwheels-jwt │ 1.0.2 │ 3,535 │ CFWheels plugin for encoding and decoding JSON ... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ UpgradeAdvisor │ upgradeadvisor │ 0.8.1 │ 3,515 │ CFWheels 2.x Upgrade Advisor ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels iCal4j │ cfwheels-ical4j │ 2.0.0 │ 3,475 │ CFWheels 2.x Plugin Date Repeats Methods via iC... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels HTMX Plugin │ cfwheels-htmx-plugin │ 1.0.4 │ 3,465 │ HTMX Plugin for CFWheels ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels JS Disable │ cfwheels-js-disable │ 1.1 │ 3,231 │ JS Disable - CFWheels Plugin ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ Authenticate This! │ cfwheels-authenticateThis │ 1.0.1 │ 3,114 │ CFWheels 2.x Adds bCrypt authentication helper ... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels LinkToDefaultTitle │ link-to-default-title │ 0.0.9 │ 3,107 │ This helper plugin will automatically display t... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels File Bin │ cfwheels-filebin │ 0.1.0 │ 3,105 │ CFWheels File Bin ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ cfwheels ckEditor plugin │ cfwheels-ckeditor-plugin │ 1.0.1 │ 3,087 │ Over-ride the textArea() function ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels Model Nesting │ cfwheels-model-nesting │ 0.0.5 │ 3,072 │ Model Nesting - fixes cfquery dot notation ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels SAML │ cfwheels-saml │ 1.0.0 │ 3,026 │ CFWheels plugin for SAML Single Sign-On ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels Disable Form Default IDs │ cfwheels-disable-form-default-id │ 0.0.2 │ 2,960 │ Disable IDs from being added to form inputs ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ TitleTag Plugin │ cfwheels-titletag-plugin │ 1.0.2 │ 2,934 │ DRY up your title tags. Allows you to define ea... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ CFWheels DotEnvSettings Plugin │ cfwheels-dotenvsettings │ 1.0.0 │ 2,734 │ DotEnvSettings Plugin for CFWheels ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ cfwheels Bootstrap Multiselect plugin │ cfwheels-bootstrapmultiselect-plugin │ 1.0 │ 2,596 │ Creates a new function to allow for a multisele... ║ +╟───────────────────────────────────────┼──────────────────────────────────────┼─────────┼───────────┼────────────────────────────────────────────────────╢ +║ wheels I18n │ Wheels-i18n │ 1.0.0 │ 173 │ Internationalization (i18n) plugin for Wheels ║ +╚═══════════════════════════════════════╧══════════════════════════════════════╧═════════╧═══════════╧════════════════════════════════════════════════════╝ + +-------------------------------------------------- + +Total plugins found: 25 +Sort order: downloads +Most popular: CFWheels Models Default Scope (4,804 downloads) + + +Commands +-------------------------------------------------- + - Install: wheels plugin install + - Details: wheels plugin info + - List installed: wheels plugin list + +[INFO]: Tip + Add --format=json for JSON output + Sort with --orderBy=name,downloads,updated``` ### Search for specific plugin @@ -89,23 +151,42 @@ wheels plugin search bcrypt **Output:** ``` -=========================================================== - Searching ForgeBox for Wheels Plugins -=========================================================== +================================================== + Searching ForgeBox for Wheels Plugins +================================================== + + +Search term: bcrypt + +Searching, please wait... + + -Search term: bcrypt +Found 1 plugin(s) +-------------------------------------------------- -Found 1 plugin: +╔═════════════════╤═════════════════╤═════════╤═══════════╤════════════════════════════════════════════════════╗ +║ Name │ Slug │ Version │ Downloads │ Description ║ +╠═════════════════╪═════════════════╪═════════╪═══════════╪════════════════════════════════════════════════════╣ +║ CFWheels bCrypt │ cfwheels-bcrypt │ 1.0.2 │ 4,788 │ CFWheels 2.x plugin helper methods for the bCry... ║ +╚═════════════════╧═════════════════╧═════════╧═══════════╧════════════════════════════════════════════════════╝ -Name Version Downloads Description ------------------------------------------------------------------------ -cfwheels-bcrypt 1.0.2 4393 CFWheels 2.x plugin helper meth... +-------------------------------------------------- ------------------------------------------------------------ +Total plugins found: 1 +Sort order: downloads +Most popular: CFWheels bCrypt (4,788 downloads) -Commands: - wheels plugin install Install a plugin - wheels plugin info View plugin details + +Commands +-------------------------------------------------- + - Install: wheels plugin install + - Details: wheels plugin info + - List installed: wheels plugin list + +[INFO]: Tip + Add --format=json for JSON output + Sort with --orderBy=name,downloads,updated ``` ### No results found @@ -116,17 +197,23 @@ wheels plugin search nonexistent **Output:** ``` -=========================================================== - Searching ForgeBox for Wheels Plugins -=========================================================== +================================================== + Searching ForgeBox for Wheels Plugins +================================================== -Search term: nonexistent -No plugins found matching 'nonexistent' +Search term: nonexistent -Try: - wheels plugin search - wheels plugin list --available +Searching, please wait... + + +[WARNING]: No plugins found matching 'nonexistent' + + +Try +-------------------------------------------------- + - wheels plugin search + - wheels plugin list --available ``` ### Sort by name diff --git a/docs/src/command-line-tools/commands/plugins/plugins-update.md b/docs/src/command-line-tools/commands/plugins/plugins-update.md index 99da73c3c9..ae2dac51d1 100644 --- a/docs/src/command-line-tools/commands/plugins/plugins-update.md +++ b/docs/src/command-line-tools/commands/plugins/plugins-update.md @@ -72,20 +72,21 @@ wheels plugin update bcrypt **Output:** ``` -=========================================================== - Updating Plugin: bcrypt -=========================================================== +================================================== + Updating Plugin: bcrypt +================================================== -Plugin: bcrypt -Current version: 0.0.4 -Latest version: 0.0.4 -=========================================================== +Plugin Information +-------------------------------------------------- +Plugin: CFWheels bCrypt +Current version: 1.0.2 +Latest version: 1.0.2 -[OK] Plugin is already at the latest version (0.0.4) +[SUCCESS]: Plugin is already at the latest version (1.0.2) -Use --force to reinstall anyway: - wheels plugin update bcrypt --force +[INFO]: Use --force to reinstall anyway + - wheels plugin update bcrypt --force ``` ### Update to specific version diff --git a/docs/src/command-line-tools/commands/security/security-scan.md b/docs/src/command-line-tools/commands/security/security-scan.md index f43ed57a35..4e2f5d18f6 100644 --- a/docs/src/command-line-tools/commands/security/security-scan.md +++ b/docs/src/command-line-tools/commands/security/security-scan.md @@ -37,6 +37,14 @@ wheels security scan 🔍 Scanning for security issues... Scan complete! +Summary: +Critical: 11 +High: 107 + +Security Scan Results +==================== +Path: C:\Users\Hp\cli_testingapp\db_app\ +Date: 2026-01-26 15:27:46 Summary: Critical: 2 From 8b1988e017e8d68bc1260ae2d7074cfb0fa62685 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 28 Jan 2026 11:12:35 +0500 Subject: [PATCH 025/405] commit: update cli docs fixes --- .../commands/analysis/analyze-code.md | 52 ++++++++++++++++++- .../commands/config/config-diff.md | 1 + 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/src/command-line-tools/commands/analysis/analyze-code.md b/docs/src/command-line-tools/commands/analysis/analyze-code.md index 40290aa1bb..b370f7d634 100644 --- a/docs/src/command-line-tools/commands/analysis/analyze-code.md +++ b/docs/src/command-line-tools/commands/analysis/analyze-code.md @@ -132,7 +132,55 @@ wheels analyze code --path=app/models --fix --report --verbose ### Console Output (Default) ``` -Analyzing code quality... +Analyzing code quality with report and verbose output... + +Configuration: + Path: C:\Users\Hp\db_app\app\models\ + Severity filter: warning + Fix mode: enabled + Output format: console + Report generation: enabled + +Scanning for files... + C:\Users\Hp\db_app\app\models\Model.cfc +Found 1 files to analyze +Analyzing file 1/1: C:\Users\Hp\db_app\app\models\Model.cfc + File has 7 lines + Running code style checks... + Running security checks... + Running performance checks... + Running best practice checks... + Running complexity analysis... + Checking naming conventions... + Detecting code smells... + Checking for deprecated functions... + Checking Wheels conventions... + Found 0 issues total, 0 after severity filter +Analyzing: [==================================================] 100% Complete! +Starting duplicate code detection... +Detecting duplicate code... Found 0 duplicate blocks + +Applying automatic fixes... +Fixed 0 issues automatically + +Re-analyzing after fixes with verbose output... +Scanning for files... + C:\Users\Hp\db_app\app\models\Model.cfc +Found 1 files to analyze +Analyzing file 1/1: C:\Users\Hp\db_app\app\models\Model.cfc + File has 7 lines + Running code style checks... + Running security checks... + Running performance checks... + Running best practice checks... + Running complexity analysis... + Checking naming conventions... + Detecting code smells... + Checking for deprecated functions... + Checking Wheels conventions... + Found 0 issues total, 0 after severity filter +Analyzing: [==================================================] 100% Complete! +Starting duplicate code detection... +Detecting duplicate code... Found 0 duplicate blocks + Scanning for files... Found 51 files to analyze Analyzing: [==================================================] 100% Complete! @@ -160,6 +208,8 @@ Deprecated Calls: 0 Grade: A (100/100) Excellent! No issues found. Your code is pristine! +Generating HTML report... +HTML report generated: C:\Users\Hp\db_app\reports\code-analysis .....html ``` ### JSON Output diff --git a/docs/src/command-line-tools/commands/config/config-diff.md b/docs/src/command-line-tools/commands/config/config-diff.md index 38c4eed0cc..02c1ec268b 100644 --- a/docs/src/command-line-tools/commands/config/config-diff.md +++ b/docs/src/command-line-tools/commands/config/config-diff.md @@ -180,6 +180,7 @@ Similarity: 75% "ENV2": "production", "COMPARISONS":{ "SETTINGS":{ + "IDENTICAL":[...], "DIFFERENT":[...], "ONLYINSECOND":[...], "ONLYINFIRST":[...] From 0e6394e48668557e686a88417aa1b9b2e35940d6 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 28 Jan 2026 14:48:48 +0500 Subject: [PATCH 026/405] commit: fixes for wheels cli generate commands Trim controller and model command arguments and fix primaryKey assignment - Trim leading spaces from controller command actions argument. - Trim model command properties argument. - Fix issue where model command primaryKey was not passed to migration and default id was used --- cli/src/commands/wheels/generate/controller.cfc | 10 ++++++++-- cli/src/commands/wheels/generate/helper.cfc | 8 ++++---- cli/src/commands/wheels/generate/model.cfc | 13 ++++++++++--- cli/src/models/ScaffoldService.cfc | 11 +++++++---- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/cli/src/commands/wheels/generate/controller.cfc b/cli/src/commands/wheels/generate/controller.cfc index d83cd3bc42..dce1a89ceb 100644 --- a/cli/src/commands/wheels/generate/controller.cfc +++ b/cli/src/commands/wheels/generate/controller.cfc @@ -56,7 +56,13 @@ component aliases="wheels g controller" extends="../base" { if (hasCustomActions) { // HIGHEST PRIORITY: Custom actions specified - actionList = listToArray(arguments.actions); + actionList = listToArray(trim(arguments.actions)); + // Remove empty elements and trim each action + actionList = actionList.map(function(action) { + return trim(action); + }).filter(function(action) { + return len(action) > 0; + }); } else if (arguments.crud) { if (arguments.api) { // API: No form actions (new, edit) @@ -144,4 +150,4 @@ component aliases="wheels g controller" extends="../base" { setExitCode(1); } } -} +} \ No newline at end of file diff --git a/cli/src/commands/wheels/generate/helper.cfc b/cli/src/commands/wheels/generate/helper.cfc index 3296524168..db55329e27 100644 --- a/cli/src/commands/wheels/generate/helper.cfc +++ b/cli/src/commands/wheels/generate/helper.cfc @@ -287,17 +287,17 @@ result = #functionList[1]#("some input"); private string function generateHighlightFunction() { var content = chr(9) & chr(9) & "// Highlight search terms in text" & chr(10); - content &= chr(9) & chr(9) & "local.searchTerm = arguments.options.term ?: """";" & chr(10); + content &= chr(9) & chr(9) & "local.term = arguments.options.term ?: """";" & chr(10); content &= chr(9) & chr(9) & "local.highlightClass = arguments.options.class ?: ""highlight"";" & chr(10); content &= chr(10); - content &= chr(9) & chr(9) & "if (!len(local.searchTerm)) {" & chr(10); + content &= chr(9) & chr(9) & "if (!len(local.term)) {" & chr(10); content &= chr(9) & chr(9) & chr(9) & "return arguments.value;" & chr(10); content &= chr(9) & chr(9) & "}" & chr(10); content &= chr(10); content &= chr(9) & chr(9) & "return reReplaceNoCase(" & chr(10); content &= chr(9) & chr(9) & chr(9) & "arguments.value," & chr(10); - content &= chr(9) & chr(9) & chr(9) & """(#local.searchTerm#)""," & chr(10); - content &= chr(9) & chr(9) & chr(9) & """\\1""," & chr(10); + content &= chr(9) & chr(9) & chr(9) & """('' & local.term & '')""," & chr(10); + content &= chr(9) & chr(9) & chr(9) & """\\1""," & chr(10); content &= chr(9) & chr(9) & chr(9) & """all""" & chr(10); content &= chr(9) & chr(9) & ");" & chr(10); return content; diff --git a/cli/src/commands/wheels/generate/model.cfc b/cli/src/commands/wheels/generate/model.cfc index 69db36bfea..c2c98d0514 100644 --- a/cli/src/commands/wheels/generate/model.cfc +++ b/cli/src/commands/wheels/generate/model.cfc @@ -131,7 +131,8 @@ component aliases='wheels g model' extends="../base" { name = arguments.name, properties = parsedProperties, baseDirectory = getCWD(), - tableName = arguments.tableName + tableName = arguments.tableName, + primaryKey = arguments.primaryKey ); } else { var actualTableName = len(arguments.tableName) ? arguments.tableName : helpers.pluralize(lCase(arguments.name)); @@ -176,7 +177,13 @@ component aliases='wheels g model' extends="../base" { var properties = []; if (len(arguments.propertiesString)) { - var propList = listToArray(arguments.propertiesString); + var propList = listToArray(trim(arguments.propertiesString)); + // Remove empty elements and trim each property + propList = propList.map(function(prop) { + return trim(prop); + }).filter(function(prop) { + return len(prop) > 0; + }); for (var prop in propList) { var parts = listToArray(prop, ":"); @@ -264,4 +271,4 @@ component aliases='wheels g model' extends="../base" { return arguments.properties; } -} +} \ No newline at end of file diff --git a/cli/src/models/ScaffoldService.cfc b/cli/src/models/ScaffoldService.cfc index a20669d64e..c4f45c3ef1 100644 --- a/cli/src/models/ScaffoldService.cfc +++ b/cli/src/models/ScaffoldService.cfc @@ -212,7 +212,8 @@ component { required string name, required array properties, string baseDirectory = "", - string tableName = "" + string tableName = "", + string primaryKey = "id" ) { var timestamp = dateFormat(now(), "yyyymmdd") & timeFormat(now(), "HHmmss"); var actualTableName = len(arguments.tableName) ? arguments.tableName : variables.helpers.pluralize(lCase(arguments.name)); @@ -231,7 +232,8 @@ component { var content = generateMigrationContentWithProperties( className = className, tableName = actualTableName, - properties = arguments.properties + properties = arguments.properties, + primaryKey = arguments.primaryKey ); // Write migration file @@ -246,7 +248,8 @@ component { private function generateMigrationContentWithProperties( required string className, required string tableName, - required array properties + required array properties, + string primaryKey = "id" ) { var content = '/*' & chr(10); content &= ' |----------------------------------------------------------------------------------------------|' & chr(10); @@ -270,7 +273,7 @@ component { content &= ' function up() {' & chr(10); content &= ' transaction {' & chr(10); content &= ' try {' & chr(10); - content &= ' t = createTable(name = ''#arguments.tableName#'', force=''false'', id=''true'', primaryKey=''id'');' & chr(10); + content &= ' t = createTable(name = ''#arguments.tableName#'', force=''false'', id=''true'', primaryKey=''#arguments.primaryKey#'');' & chr(10); // Add properties (skip association properties that don't have database columns) for (var prop in arguments.properties) { From 4a102fcc43f2d603fc000bf8a7c3493836dd33f3 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 28 Jan 2026 19:10:38 +0500 Subject: [PATCH 027/405] commit: update wheels about command --- cli/src/commands/wheels/about.cfc | 4 +-- docs/src/SUMMARY.md | 1 + .../commands/application-utilities/about.md | 29 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cli/src/commands/wheels/about.cfc b/cli/src/commands/wheels/about.cfc index e05f38378a..6f6f141405 100644 --- a/cli/src/commands/wheels/about.cfc +++ b/cli/src/commands/wheels/about.cfc @@ -90,8 +90,8 @@ component extends="base" { // Helpful Links print.boldGreenLine("Resources"); - print.cyanLine(" Documentation: https://wheels.dev/docs"); - print.cyanLine(" API Reference: https://wheels.dev/api"); + print.cyanLine(" Documentation: https://wheels.dev/guides"); + print.cyanLine(" API Reference: https://wheels.dev/api/v3.0.0"); print.cyanLine(" GitHub: https://github.com/wheels-dev/wheels"); print.cyanLine(" Community: https://wheels.dev/community"); print.line(); diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 0ab81473a7..1b5776a9ba 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -20,6 +20,7 @@ * Core Commands * [wheels init](command-line-tools/commands/core/init.md) * [wheels info](command-line-tools/commands/core/info.md) + * [wheels about](command-line-tools/commands/application-utilities/about.md) * [wheels reload](command-line-tools/commands/core/reload.md) * [wheels deps](command-line-tools/commands/core/deps.md) * [wheels destroy](command-line-tools/commands/core/destroy.md) diff --git a/docs/src/command-line-tools/commands/application-utilities/about.md b/docs/src/command-line-tools/commands/application-utilities/about.md index 38cacd987d..510e34567f 100644 --- a/docs/src/command-line-tools/commands/application-utilities/about.md +++ b/docs/src/command-line-tools/commands/application-utilities/about.md @@ -26,41 +26,40 @@ wheels about Output: ``` - \ \ / / | | | - \ \ /\ / /| |__ ___ ___| |___ + __ ___ _ + \ \ / / | | | + \ \ /\ / /| |__ ___ ___| |___ \ \/ \/ / | '_ \ / _ \/ _ \ / __| \ /\ / | | | | __/ __/ \__ \ \/ \/ |_| |_|\___|\___|_|___/ Wheels Framework - Version: 2.5.0 + Version: 3.0.0 Wheels CLI - Version: 1.0.0 + Version: 3.0.0 Location: /commandbox/modules/wheels-cli/ Application Path: /Users/developer/myapp - Name: MyWheelsApp Environment: development Database: Configured Server Environment - CFML Engine: Lucee 5.4.1.8 - Java Version: 11.0.15 - OS: macOS 13.0 + CFML Engine: Lucee 7.0.1+100 + Java Version: 17.0.17 + OS: Mac OS X 15.7.3 Architecture: x86_64 CommandBox - Version: 5.9.0 - Home: /Users/developer/.CommandBox + Version: 6.3.1+00853 Application Statistics - Controllers: 12 - Models: 8 - Views: 45 - Tests: 23 - Migrations: 15 + Controllers: 15 + Models: 7 + Views: 31 + Tests: 3 + Migrations: 9 Resources Documentation: https://wheels.dev/guides From b84bce7f4200c59f561252227e312821acf891b0 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 28 Jan 2026 21:02:15 +0500 Subject: [PATCH 028/405] Rename 'snippets' command to 'code' across documentation and implementation Updated the `wheels generate snippets` command to `wheels generate code` to better reflect its purpose of generating code snippets for common Wheels patterns. Changes: - Renamed command alias from "wheels g snippets" to "wheels g code" - Updated all user-facing strings and messages to use "code" terminology - Modified documentation to reflect the new command name - Updated help text, error messages, and success notifications - Changed customization options text to reference "code snippets" - Updated all usage examples in comments and output Technical details: - Component alias updated: `aliases="wheels g code"` - All string literals mentioning "snippet" replaced with "code snippet" where user-facing - Function and variable names remain unchanged for backward compatibility - Documentation completely rewritten to match new command name This change improves clarity by explicitly naming the command after what it generates (code), while maintaining all existing functionality. --- cli/src/commands/wheels/generate/code.cfc | 437 +++++++++++++++ cli/src/commands/wheels/generate/snippets.cfc | 440 +-------------- docs/src/SUMMARY.md | 1 + .../src/command-line-tools/commands/README.md | 5 +- .../commands/generate/code.md | 502 ++++++++++++++++++ .../commands/generate/snippets.md | 486 +---------------- 6 files changed, 978 insertions(+), 893 deletions(-) create mode 100644 cli/src/commands/wheels/generate/code.cfc create mode 100644 docs/src/command-line-tools/commands/generate/code.md diff --git a/cli/src/commands/wheels/generate/code.cfc b/cli/src/commands/wheels/generate/code.cfc new file mode 100644 index 0000000000..04982a6f5e --- /dev/null +++ b/cli/src/commands/wheels/generate/code.cfc @@ -0,0 +1,437 @@ +/** + * Generate code snippets for common patterns + * + * Examples: + * wheels g code auth-filter + * wheels g code --list + * wheels g code --category=model + */ +component aliases="wheels g code" extends="../base" { + + property name="detailOutput" inject="DetailOutputService@wheels-cli"; + + /** + * @pattern.hint Code pattern name + * @list.hint Show all available code snippets + * @category.hint Filter by category (authentication, model, controller, view, database) + * @output.hint Output format (console or file) + * @path.hint Output path (required for file output) + * @customize.hint Create custom code snippet + * @create.hint Create new code template + * @force.hint Overwrite existing files + */ + function run( + required string pattern, + boolean list = false, + string category = "", + string output = "console", + string path = "", + boolean customize = false, + boolean create = false, + boolean force = false + ) { + requireWheelsApp(getCWD()); + arguments = reconstructArgs( + argStruct=arguments, + allowedValues={ + output: ["console", "file"], + category: ["authentication", "model", "controller", "view", "database"] + } + ); + + if (arguments.list) { + return listSnippets(arguments.category); + } + + if (arguments.create) { + return createCustomSnippet(arguments.pattern); + } + + if (arguments.customize) { + return showCustomizationOptions(); + } + + if (!len(arguments.pattern)) { + detailOutput.error("Pattern name is required"); + detailOutput.getPrint().line("Usage: wheels g code "); + detailOutput.getPrint().line("Run 'wheels g code --list' to see available patterns"); + setExitCode(1); + return; + } + + var snippet = getSnippetByName(arguments.pattern); + if (!structCount(snippet)) { + detailOutput.error("Code snippet '#arguments.pattern#' not found"); + detailOutput.getPrint().line("Run 'wheels g code --list' to see available patterns"); + setExitCode(1); + return; + } + + if (arguments.output == "file") { + if (!len(arguments.path)) { + detailOutput.error("--path is required when using --output=file"); + setExitCode(1); + return; + } + writeSnippetToFile(snippet, arguments.path, arguments.force); + } else { + printSnippet(snippet); + } + } + + /** + * List available snippets + */ + private function listSnippets(string category = "") { + var snippets = getAvailableSnippets(); + var categories = {}; + + for (var snippet in snippets) { + if (len(arguments.category) && snippet.category != arguments.category) { + continue; + } + if (!structKeyExists(categories, snippet.category)) { + categories[snippet.category] = []; + } + arrayAppend(categories[snippet.category], snippet); + } + + detailOutput.header("Available Code Snippets"); + + var categoryOrder = ["Authentication", "Model", "Controller", "View", "Database"]; + for (var cat in categoryOrder) { + var key = lCase(cat); + if (structKeyExists(categories, key)) { + detailOutput.getPrint().line(""); + detailOutput.getPrint().boldLine("#cat#:"); + for (var snippet in categories[key]) { + detailOutput.getPrint().line(" - #snippet.name# - #snippet.description#"); + } + } + } + + detailOutput.getPrint().line(""); + detailOutput.getPrint().line(""); + detailOutput.nextSteps([ + "Generate a code snippet: wheels g code " + ]); + } + + /** + * Print snippet to console + */ + private function printSnippet(required struct snippet) { + detailOutput.header("Generating Code Snippet: #arguments.snippet.name#"); + detailOutput.getPrint().line(""); + + var content = getSnippetContent(arguments.snippet); + detailOutput.getPrint().line(content); + detailOutput.getPrint().line(""); + + detailOutput.success("Code snippet '#arguments.snippet.name#' generated successfully!"); + } + + /** + * Write snippet to file + */ + private function writeSnippetToFile(required struct snippet, required string path, boolean force = false) { + // Resolve path relative to application directory + var resolvedPath = fileSystemUtil.resolvePath(arguments.path); + + if (fileExists(resolvedPath) && !arguments.force) { + detailOutput.error("File already exists: #resolvedPath#"); + detailOutput.getPrint().line("Use --force to overwrite"); + setExitCode(1); + return; + } + + var content = getSnippetContent(arguments.snippet); + + // Create directory if needed + var dir = getDirectoryFromPath(resolvedPath); + if (!directoryExists(dir)) { + directoryCreate(dir, true); + } + + fileWrite(resolvedPath, content); + detailOutput.create("Created: #resolvedPath#"); + } + + /** + * Get available snippets + */ + private function getAvailableSnippets() { + return [ + {name: "login-form", category: "authentication", description: "Login form with remember me"}, + {name: "auth-filter", category: "authentication", description: "Authentication filter"}, + {name: "password-reset", category: "authentication", description: "Password reset flow"}, + {name: "user-registration", category: "authentication", description: "User registration with validation"}, + {name: "soft-delete", category: "model", description: "Soft delete implementation"}, + {name: "audit-trail", category: "model", description: "Audit trail with timestamps"}, + {name: "sluggable", category: "model", description: "URL-friendly slugs"}, + {name: "versionable", category: "model", description: "Version tracking"}, + {name: "searchable", category: "model", description: "Full-text search"}, + {name: "crud-actions", category: "controller", description: "Complete CRUD actions"}, + {name: "api-controller", category: "controller", description: "JSON API controller"}, + {name: "nested-resource", category: "controller", description: "Nested resource controller"}, + {name: "admin-controller", category: "controller", description: "Admin area controller"}, + {name: "form-with-errors", category: "view", description: "Form with error handling"}, + {name: "pagination-links", category: "view", description: "Pagination navigation"}, + {name: "search-form", category: "view", description: "Search form with filters"}, + {name: "ajax-form", category: "view", description: "AJAX form submission"}, + {name: "migration-indexes", category: "database", description: "Common index patterns"}, + {name: "seed-data", category: "database", description: "Database seeding"}, + {name: "constraints", category: "database", description: "Foreign key constraints"} + ]; + } + + /** + * Get snippet by name + */ + private function getSnippetByName(required string name) { + var snippets = getAvailableSnippets(); + for (var snippet in snippets) { + if (snippet.name == arguments.name) { + return snippet; + } + } + return {}; + } + + /** + * Get snippet content + */ + private function getSnippetContent(required struct snippet) { + switch (arguments.snippet.name) { + case "login-form": + return '##startFormTag(action="create")##' & chr(10) & + ' ##textField(objectName="user", property="email", label="Email")##' & chr(10) & + ' ##passwordField(objectName="user", property="password", label="Password")##' & chr(10) & + ' ##checkBox(objectName="user", property="rememberMe", label="Remember me")##' & chr(10) & + ' ##submitTag(value="Login")##' & chr(10) & + '##endFormTag()##'; + + case "auth-filter": + return 'function init() {' & chr(10) & + ' filters(through="authenticate", except="new,create");' & chr(10) & + '}' & chr(10) & chr(10) & + 'private function authenticate() {' & chr(10) & + ' if (!StructKeyExists(session, "userId")) {' & chr(10) & + ' redirectTo(route="login");' & chr(10) & + ' }' & chr(10) & + '}'; + + case "password-reset": + return 'function requestReset() {' & chr(10) & + ' user = model("User").findOne(where="email=''##params.email##''");' & chr(10) & + ' if (IsObject(user)) {' & chr(10) & + ' token = Hash(CreateUUID());' & chr(10) & + ' user.update(resetToken=token, resetExpiresAt=DateAdd("h", 1, Now()));' & chr(10) & + ' // Send email with token' & chr(10) & + ' }' & chr(10) & + '}'; + + case "user-registration": + return '##startFormTag(action="create")##' & chr(10) & + ' ##textField(objectName="user", property="firstName", label="First Name")##' & chr(10) & + ' ##textField(objectName="user", property="email", label="Email")##' & chr(10) & + ' ##passwordField(objectName="user", property="password", label="Password")##' & chr(10) & + ' ##submitTag(value="Register")##' & chr(10) & + '##endFormTag()##'; + + case "soft-delete": + return 'function init() {' & chr(10) & + ' property(name="deletedAt", sql="deleted_at");' & chr(10) & + ' beforeDelete("softDelete");' & chr(10) & + '}' & chr(10) & chr(10) & + 'private function softDelete() {' & chr(10) & + ' this.deletedAt = Now();' & chr(10) & + ' this.save(validate=false, callbacks=false);' & chr(10) & + ' return false;' & chr(10) & + '}'; + + case "audit-trail": + return 'function init() {' & chr(10) & + ' property(name="createdBy", sql="created_by");' & chr(10) & + ' property(name="updatedBy", sql="updated_by");' & chr(10) & + ' beforeSave("setAuditFields");' & chr(10) & + '}' & chr(10) & chr(10) & + 'private function setAuditFields() {' & chr(10) & + ' if (StructKeyExists(session, "userId")) {' & chr(10) & + ' if (this.isNew()) this.createdBy = session.userId;' & chr(10) & + ' this.updatedBy = session.userId;' & chr(10) & + ' }' & chr(10) & + '}'; + + case "sluggable": + return 'function init() {' & chr(10) & + ' property(name="slug");' & chr(10) & + ' beforeSave("generateSlug");' & chr(10) & + '}' & chr(10) & chr(10) & + 'private function generateSlug() {' & chr(10) & + ' if (!len(this.slug) && len(this.title)) {' & chr(10) & + ' this.slug = lCase(reReplace(this.title, "[^a-zA-Z0-9]", "-", "all"));' & chr(10) & + ' }' & chr(10) & + '}'; + + case "versionable": + return 'function init() {' & chr(10) & + ' property(name="version", default=1);' & chr(10) & + ' beforeUpdate("incrementVersion");' & chr(10) & + '}' & chr(10) & chr(10) & + 'private function incrementVersion() {' & chr(10) & + ' this.version = this.version + 1;' & chr(10) & + '}'; + + case "searchable": + return 'function search(required string query) {' & chr(10) & + ' return findAll(where="title LIKE ''%##arguments.query##%'' OR content LIKE ''%##arguments.query##%''");' & chr(10) & + '}'; + + case "crud-actions": + return 'function index() {' & chr(10) & + ' users = model("User").findAll();' & chr(10) & + '}' & chr(10) & chr(10) & + 'function show() {' & chr(10) & + ' user = model("User").findByKey(params.key);' & chr(10) & + '}' & chr(10) & chr(10) & + 'function create() {' & chr(10) & + ' user = model("User").create(params.user);' & chr(10) & + ' if (user.valid()) {' & chr(10) & + ' redirectTo(route="user", key=user.id);' & chr(10) & + ' } else {' & chr(10) & + ' renderView(action="new");' & chr(10) & + ' }' & chr(10) & + '}'; + + case "api-controller": + return 'function init() {' & chr(10) & + ' provides("json");' & chr(10) & + '}' & chr(10) & chr(10) & + 'function index() {' & chr(10) & + ' users = model("User").findAll();' & chr(10) & + ' renderWith(data={users=users});' & chr(10) & + '}'; + + case "nested-resource": + return 'function init() {' & chr(10) & + ' filters(through="findParent");' & chr(10) & + '}' & chr(10) & chr(10) & + 'private function findParent() {' & chr(10) & + ' user = model("User").findByKey(params.userId);' & chr(10) & + '}'; + + case "admin-controller": + return 'function init() {' & chr(10) & + ' filters(through="requireAdmin");' & chr(10) & + '}' & chr(10) & chr(10) & + 'private function requireAdmin() {' & chr(10) & + ' if (!currentUser().isAdmin()) {' & chr(10) & + ' redirectTo(route="home");' & chr(10) & + ' }' & chr(10) & + '}'; + + case "form-with-errors": + return '##errorMessagesFor("user")##' & chr(10) & chr(10) & + '##startFormTag(action="create")##' & chr(10) & + ' ##textField(objectName="user", property="firstName", label="First Name")##' & chr(10) & + ' ' & chr(10) & + ' ##user.errors("firstName").get()##' & chr(10) & + ' ' & chr(10) & + ' ##submitTag(value="Submit")##' & chr(10) & + '##endFormTag()##'; + + case "pagination-links": + return '' & chr(10) & + ' ' & chr(10) & + ''; + + case "search-form": + return '##startFormTag(method="get")##' & chr(10) & + ' ##textField(name="q", value=params.q, placeholder="Search...")##' & chr(10) & + ' ##submitTag(value="Search")##' & chr(10) & + '##endFormTag()##'; + + case "ajax-form": + return '##startFormTag(action="create", id="userForm")##' & chr(10) & + ' ##textField(objectName="user", property="name")##' & chr(10) & + ' ##submitTag(value="Submit")##' & chr(10) & + '##endFormTag()##' & chr(10) & chr(10) & + ''; + + case "migration-indexes": + return 't.index("email");' & chr(10) & + 't.index(["last_name", "first_name"]);' & chr(10) & + 't.index("email", unique=true);' & chr(10) & + 't.index("user_id");'; + + case "seed-data": + return 'execute("INSERT INTO users (name, email) VALUES (''Admin'', ''admin@example.com'')");"'; + + case "constraints": + return 't.references("user_id", "users");' & chr(10) & + 't.references("category_id", "categories");'; + + default: + return "Code snippet not found"; + } + } + + /** + * Create custom snippet + */ + private function createCustomSnippet(required string name) { + detailOutput.header("Creating Custom Code Snippet"); + + var snippetDir = getCWD() & "/app/snippets/" & arguments.name; + + if (directoryExists(snippetDir)) { + detailOutput.error("Code snippet '#arguments.name#' already exists"); + setExitCode(1); + return; + } + + // Create directory + directoryCreate(snippetDir, true); + + // Create basic template file + var templateContent = '// Custom code snippet: #arguments.name#' & chr(10) & + '// Add your code here'; + + fileWrite(snippetDir & "/template.txt", templateContent); + + detailOutput.create("Created: #snippetDir#"); + detailOutput.success("Custom code snippet '#arguments.name#' created successfully!"); + + detailOutput.nextSteps([ + "Edit: #snippetDir#/template.txt", + "Use: wheels g code #arguments.name#" + ]); + } + + /** + * Show customization options + */ + private function showCustomizationOptions() { + detailOutput.header("Customization Options"); + detailOutput.getPrint().line("You can customize code snippets by:"); + detailOutput.getPrint().line(" 1. Creating custom code snippets with --create"); + detailOutput.getPrint().line(" 2. Saving code snippets to files with --output=file"); + detailOutput.getPrint().line(" 3. Filtering by category with --category"); + } +} diff --git a/cli/src/commands/wheels/generate/snippets.cfc b/cli/src/commands/wheels/generate/snippets.cfc index 3919eebd66..564c902fd7 100644 --- a/cli/src/commands/wheels/generate/snippets.cfc +++ b/cli/src/commands/wheels/generate/snippets.cfc @@ -1,437 +1,23 @@ /** - * Generate code snippets for common patterns - * - * Examples: - * wheels g snippets auth-filter - * wheels g snippets --list - * wheels g snippets --category=model + * Copies the template snippets to the application. */ -component aliases="wheels g snippets" extends="../base" { +component aliases="wheels g snippets" extends="../base"{ - property name="detailOutput" inject="DetailOutputService@wheels-cli"; + function run() + { + requireWheelsApp(getCWD()); + arguments.directory = fileSystemUtil.resolvePath( 'app' ); - /** - * @pattern.hint Snippet pattern name - * @list.hint Show all available snippets - * @category.hint Filter by category (authentication, model, controller, view, database) - * @output.hint Output format (console or file) - * @path.hint Output path (required for file output) - * @customize.hint Create custom snippet - * @create.hint Create new snippet template - * @force.hint Overwrite existing files - */ - function run( - required string pattern, - boolean list = false, - string category = "", - string output = "console", - string path = "", - boolean customize = false, - boolean create = false, - boolean force = false - ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs( - argStruct=arguments, - allowedValues={ - output: ["console", "file"], - category: ["authentication", "model", "controller", "view", "database"] - } - ); + print.line('Starting snippet generation...').toConsole(); - if (arguments.list) { - return listSnippets(arguments.category); - } - - if (arguments.create) { - return createCustomSnippet(arguments.pattern); - } - - if (arguments.customize) { - return showCustomizationOptions(); - } - - if (!len(arguments.pattern)) { - detailOutput.error("Pattern name is required"); - detailOutput.getPrint().line("Usage: wheels g snippets "); - detailOutput.getPrint().line("Run 'wheels g snippets --list' to see available patterns"); - setExitCode(1); - return; - } - - var snippet = getSnippetByName(arguments.pattern); - if (!structCount(snippet)) { - detailOutput.error("Snippet '#arguments.pattern#' not found"); - detailOutput.getPrint().line("Run 'wheels g snippets --list' to see available patterns"); - setExitCode(1); - return; - } - - if (arguments.output == "file") { - if (!len(arguments.path)) { - detailOutput.error("--path is required when using --output=file"); - setExitCode(1); - return; - } - writeSnippetToFile(snippet, arguments.path, arguments.force); - } else { - printSnippet(snippet); - } - } - - /** - * List available snippets - */ - private function listSnippets(string category = "") { - var snippets = getAvailableSnippets(); - var categories = {}; - - for (var snippet in snippets) { - if (len(arguments.category) && snippet.category != arguments.category) { - continue; - } - if (!structKeyExists(categories, snippet.category)) { - categories[snippet.category] = []; - } - arrayAppend(categories[snippet.category], snippet); - } - - detailOutput.header("Available Snippets"); - - var categoryOrder = ["Authentication", "Model", "Controller", "View", "Database"]; - for (var cat in categoryOrder) { - var key = lCase(cat); - if (structKeyExists(categories, key)) { - detailOutput.getPrint().line(""); - detailOutput.getPrint().boldLine("#cat#:"); - for (var snippet in categories[key]) { - detailOutput.getPrint().line(" - #snippet.name# - #snippet.description#"); - } - } - } - - detailOutput.getPrint().line(""); - detailOutput.getPrint().line(""); - detailOutput.nextSteps([ - "Generate a snippet: wheels g snippets " - ]); - } - - /** - * Print snippet to console - */ - private function printSnippet(required struct snippet) { - detailOutput.header("Generating Snippet: #arguments.snippet.name#"); - detailOutput.getPrint().line(""); - - var content = getSnippetContent(arguments.snippet); - detailOutput.getPrint().line(content); - detailOutput.getPrint().line(""); - - detailOutput.success("Snippet '#arguments.snippet.name#' generated successfully!"); - } - - /** - * Write snippet to file - */ - private function writeSnippetToFile(required struct snippet, required string path, boolean force = false) { - // Resolve path relative to application directory - var resolvedPath = fileSystemUtil.resolvePath(arguments.path); - - if (fileExists(resolvedPath) && !arguments.force) { - detailOutput.error("File already exists: #resolvedPath#"); - detailOutput.getPrint().line("Use --force to overwrite"); - setExitCode(1); - return; - } - - var content = getSnippetContent(arguments.snippet); - - // Create directory if needed - var dir = getDirectoryFromPath(resolvedPath); - if (!directoryExists(dir)) { - directoryCreate(dir, true); - } - - fileWrite(resolvedPath, content); - detailOutput.create("Created: #resolvedPath#"); + // Validate the provided directory + if (!directoryExists(arguments.directory)) { + error('[#arguments.directory#] can''t be found. Are you running this command from your application root?'); } - /** - * Get available snippets - */ - private function getAvailableSnippets() { - return [ - {name: "login-form", category: "authentication", description: "Login form with remember me"}, - {name: "auth-filter", category: "authentication", description: "Authentication filter"}, - {name: "password-reset", category: "authentication", description: "Password reset flow"}, - {name: "user-registration", category: "authentication", description: "User registration with validation"}, - {name: "soft-delete", category: "model", description: "Soft delete implementation"}, - {name: "audit-trail", category: "model", description: "Audit trail with timestamps"}, - {name: "sluggable", category: "model", description: "URL-friendly slugs"}, - {name: "versionable", category: "model", description: "Version tracking"}, - {name: "searchable", category: "model", description: "Full-text search"}, - {name: "crud-actions", category: "controller", description: "Complete CRUD actions"}, - {name: "api-controller", category: "controller", description: "JSON API controller"}, - {name: "nested-resource", category: "controller", description: "Nested resource controller"}, - {name: "admin-controller", category: "controller", description: "Admin area controller"}, - {name: "form-with-errors", category: "view", description: "Form with error handling"}, - {name: "pagination-links", category: "view", description: "Pagination navigation"}, - {name: "search-form", category: "view", description: "Search form with filters"}, - {name: "ajax-form", category: "view", description: "AJAX form submission"}, - {name: "migration-indexes", category: "database", description: "Common index patterns"}, - {name: "seed-data", category: "database", description: "Database seeding"}, - {name: "constraints", category: "database", description: "Foreign key constraints"} - ]; - } - - /** - * Get snippet by name - */ - private function getSnippetByName(required string name) { - var snippets = getAvailableSnippets(); - for (var snippet in snippets) { - if (snippet.name == arguments.name) { - return snippet; - } - } - return {}; - } - - /** - * Get snippet content - */ - private function getSnippetContent(required struct snippet) { - switch (arguments.snippet.name) { - case "login-form": - return '##startFormTag(action="create")##' & chr(10) & - ' ##textField(objectName="user", property="email", label="Email")##' & chr(10) & - ' ##passwordField(objectName="user", property="password", label="Password")##' & chr(10) & - ' ##checkBox(objectName="user", property="rememberMe", label="Remember me")##' & chr(10) & - ' ##submitTag(value="Login")##' & chr(10) & - '##endFormTag()##'; - - case "auth-filter": - return 'function init() {' & chr(10) & - ' filters(through="authenticate", except="new,create");' & chr(10) & - '}' & chr(10) & chr(10) & - 'private function authenticate() {' & chr(10) & - ' if (!StructKeyExists(session, "userId")) {' & chr(10) & - ' redirectTo(route="login");' & chr(10) & - ' }' & chr(10) & - '}'; - - case "password-reset": - return 'function requestReset() {' & chr(10) & - ' user = model("User").findOne(where="email=''##params.email##''");' & chr(10) & - ' if (IsObject(user)) {' & chr(10) & - ' token = Hash(CreateUUID());' & chr(10) & - ' user.update(resetToken=token, resetExpiresAt=DateAdd("h", 1, Now()));' & chr(10) & - ' // Send email with token' & chr(10) & - ' }' & chr(10) & - '}'; - - case "user-registration": - return '##startFormTag(action="create")##' & chr(10) & - ' ##textField(objectName="user", property="firstName", label="First Name")##' & chr(10) & - ' ##textField(objectName="user", property="email", label="Email")##' & chr(10) & - ' ##passwordField(objectName="user", property="password", label="Password")##' & chr(10) & - ' ##submitTag(value="Register")##' & chr(10) & - '##endFormTag()##'; + ensureSnippetTemplatesExist(); - case "soft-delete": - return 'function init() {' & chr(10) & - ' property(name="deletedAt", sql="deleted_at");' & chr(10) & - ' beforeDelete("softDelete");' & chr(10) & - '}' & chr(10) & chr(10) & - 'private function softDelete() {' & chr(10) & - ' this.deletedAt = Now();' & chr(10) & - ' this.save(validate=false, callbacks=false);' & chr(10) & - ' return false;' & chr(10) & - '}'; + print.line('Snippet successfully generated in the /app/snippets folder.').toConsole(); + } - case "audit-trail": - return 'function init() {' & chr(10) & - ' property(name="createdBy", sql="created_by");' & chr(10) & - ' property(name="updatedBy", sql="updated_by");' & chr(10) & - ' beforeSave("setAuditFields");' & chr(10) & - '}' & chr(10) & chr(10) & - 'private function setAuditFields() {' & chr(10) & - ' if (StructKeyExists(session, "userId")) {' & chr(10) & - ' if (this.isNew()) this.createdBy = session.userId;' & chr(10) & - ' this.updatedBy = session.userId;' & chr(10) & - ' }' & chr(10) & - '}'; - - case "sluggable": - return 'function init() {' & chr(10) & - ' property(name="slug");' & chr(10) & - ' beforeSave("generateSlug");' & chr(10) & - '}' & chr(10) & chr(10) & - 'private function generateSlug() {' & chr(10) & - ' if (!len(this.slug) && len(this.title)) {' & chr(10) & - ' this.slug = lCase(reReplace(this.title, "[^a-zA-Z0-9]", "-", "all"));' & chr(10) & - ' }' & chr(10) & - '}'; - - case "versionable": - return 'function init() {' & chr(10) & - ' property(name="version", default=1);' & chr(10) & - ' beforeUpdate("incrementVersion");' & chr(10) & - '}' & chr(10) & chr(10) & - 'private function incrementVersion() {' & chr(10) & - ' this.version = this.version + 1;' & chr(10) & - '}'; - - case "searchable": - return 'function search(required string query) {' & chr(10) & - ' return findAll(where="title LIKE ''%##arguments.query##%'' OR content LIKE ''%##arguments.query##%''");' & chr(10) & - '}'; - - case "crud-actions": - return 'function index() {' & chr(10) & - ' users = model("User").findAll();' & chr(10) & - '}' & chr(10) & chr(10) & - 'function show() {' & chr(10) & - ' user = model("User").findByKey(params.key);' & chr(10) & - '}' & chr(10) & chr(10) & - 'function create() {' & chr(10) & - ' user = model("User").create(params.user);' & chr(10) & - ' if (user.valid()) {' & chr(10) & - ' redirectTo(route="user", key=user.id);' & chr(10) & - ' } else {' & chr(10) & - ' renderView(action="new");' & chr(10) & - ' }' & chr(10) & - '}'; - - case "api-controller": - return 'function init() {' & chr(10) & - ' provides("json");' & chr(10) & - '}' & chr(10) & chr(10) & - 'function index() {' & chr(10) & - ' users = model("User").findAll();' & chr(10) & - ' renderWith(data={users=users});' & chr(10) & - '}'; - - case "nested-resource": - return 'function init() {' & chr(10) & - ' filters(through="findParent");' & chr(10) & - '}' & chr(10) & chr(10) & - 'private function findParent() {' & chr(10) & - ' user = model("User").findByKey(params.userId);' & chr(10) & - '}'; - - case "admin-controller": - return 'function init() {' & chr(10) & - ' filters(through="requireAdmin");' & chr(10) & - '}' & chr(10) & chr(10) & - 'private function requireAdmin() {' & chr(10) & - ' if (!currentUser().isAdmin()) {' & chr(10) & - ' redirectTo(route="home");' & chr(10) & - ' }' & chr(10) & - '}'; - - case "form-with-errors": - return '##errorMessagesFor("user")##' & chr(10) & chr(10) & - '##startFormTag(action="create")##' & chr(10) & - ' ##textField(objectName="user", property="firstName", label="First Name")##' & chr(10) & - ' ' & chr(10) & - ' ##user.errors("firstName").get()##' & chr(10) & - ' ' & chr(10) & - ' ##submitTag(value="Submit")##' & chr(10) & - '##endFormTag()##'; - - case "pagination-links": - return '' & chr(10) & - ' ' & chr(10) & - ''; - - case "search-form": - return '##startFormTag(method="get")##' & chr(10) & - ' ##textField(name="q", value=params.q, placeholder="Search...")##' & chr(10) & - ' ##submitTag(value="Search")##' & chr(10) & - '##endFormTag()##'; - - case "ajax-form": - return '##startFormTag(action="create", id="userForm")##' & chr(10) & - ' ##textField(objectName="user", property="name")##' & chr(10) & - ' ##submitTag(value="Submit")##' & chr(10) & - '##endFormTag()##' & chr(10) & chr(10) & - ''; - - case "migration-indexes": - return 't.index("email");' & chr(10) & - 't.index(["last_name", "first_name"]);' & chr(10) & - 't.index("email", unique=true);' & chr(10) & - 't.index("user_id");'; - - case "seed-data": - return 'execute("INSERT INTO users (name, email) VALUES (''Admin'', ''admin@example.com'')");"'; - - case "constraints": - return 't.references("user_id", "users");' & chr(10) & - 't.references("category_id", "categories");'; - - default: - return "Snippet not found"; - } - } - - /** - * Create custom snippet - */ - private function createCustomSnippet(required string name) { - detailOutput.header("Creating Custom Snippet"); - - var snippetDir = getCWD() & "/app/snippets/" & arguments.name; - - if (directoryExists(snippetDir)) { - detailOutput.error("Snippet '#arguments.name#' already exists"); - setExitCode(1); - return; - } - - // Create directory - directoryCreate(snippetDir, true); - - // Create basic template file - var templateContent = '// Custom snippet: #arguments.name#' & chr(10) & - '// Add your code here'; - - fileWrite(snippetDir & "/template.txt", templateContent); - - detailOutput.create("Created: #snippetDir#"); - detailOutput.success("Custom snippet '#arguments.name#' created successfully!"); - - detailOutput.nextSteps([ - "Edit: #snippetDir#/template.txt", - "Use: wheels g snippets #arguments.name#" - ]); - } - - /** - * Show customization options - */ - private function showCustomizationOptions() { - detailOutput.header("Customization Options"); - detailOutput.getPrint().line("You can customize snippets by:"); - detailOutput.getPrint().line(" 1. Creating custom snippets with --create"); - detailOutput.getPrint().line(" 2. Saving snippets to files with --output=file"); - detailOutput.getPrint().line(" 3. Filtering by category with --category"); - } } \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 0ab81473a7..5f9746d8fd 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -35,6 +35,7 @@ * [wheels generate route](command-line-tools/commands/generate/route.md) * [wheels generate test](command-line-tools/commands/generate/test.md) * [wheels generate snippets](command-line-tools/commands/generate/snippets.md) + * [wheels generate code](command-line-tools/commands/generate/code.md) * [wheels generate scaffold](command-line-tools/commands/generate/scaffold.md) * [wheels generate api-resource](command-line-tools/commands/generate/api-resource.md) * Database Commands diff --git a/docs/src/command-line-tools/commands/README.md b/docs/src/command-line-tools/commands/README.md index 9572c2671c..8509eb408e 100644 --- a/docs/src/command-line-tools/commands/README.md +++ b/docs/src/command-line-tools/commands/README.md @@ -67,7 +67,10 @@ Commands for generating application code and resources. - **`wheels generate test`** - Generate tests [Documentation](generate/test.md) -- **`wheels generate snippets`** - Code snippets +- **`wheels generate code`** - Code snippets + [Documentation](generate/code.md) + +- **`wheels generate snippets`** - Snippets Template [Documentation](generate/snippets.md) - **`wheels generate scaffold`** - Complete CRUD diff --git a/docs/src/command-line-tools/commands/generate/code.md b/docs/src/command-line-tools/commands/generate/code.md new file mode 100644 index 0000000000..6bb58ba43b --- /dev/null +++ b/docs/src/command-line-tools/commands/generate/code.md @@ -0,0 +1,502 @@ +# wheels generate code + +Generate code snippets and boilerplate code for common Wheels patterns. + +## Synopsis + +```bash +wheels generate code [pattern] [options] +wheels g code [pattern] [options] +``` + +## Description + +The `wheels generate code` 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. + +## Arguments + +| Argument | Description | Default | +|----------|-------------|---------| +| `pattern` | Code pattern to generate | Shows available patterns | + +## Options + +| Option | Description | Valid Values | Default | +|--------|-------------|--------------|---------| +| `--list` | List all available code snippets | `true`/`false` | `false` | +| `--category` | Filter by category | `authentication`, `model`, `controller`, `view`, `database` | All categories | +| `--output` | Output format | `console`, `file` | `console` | +| `--path` | Output file path (required when output=file) | Any valid file path | | +| `--customize` | Show customization options | `true`/`false` | `false` | +| `--create` | Create custom code template | `true`/`false` | `false` | +| `--force` | Overwrite existing files | `true`/`false` | `false` | + +## Available Code Snippets + +### List All Code Snippets +```bash +wheels generate code --list +``` + +Output: +``` +================================================== + Available Code 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: + - soft-delete - Soft delete implementation + - audit-trail - Audit trail with timestamps + - sluggable - URL-friendly slugs + - versionable - Version tracking + - searchable - Full-text search + +Controller: + - crud-actions - Complete CRUD actions + - api-controller - JSON API controller + - nested-resource - Nested resource controller + - admin-controller - Admin area controller + +View: + - 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 + + +Next steps: + 1. Generate a code snippet: wheels g code +``` + +## Authentication Snippets + +### Login Form +```bash +wheels generate code login-form +``` + +Generates: +```cfm +#startFormTag(action="create")# + #textField(objectName="user", property="email", label="Email")# + #passwordField(objectName="user", property="password", label="Password")# + #checkBox(objectName="user", property="rememberMe", label="Remember me")# + #submitTag(value="Login")# +#endFormTag()# +``` + +**Note**: This is a basic snippet. You can customize it by saving to a file and editing: +```bash +wheels generate code login-form --output=file --path=app/views/sessions/new.cfm +``` + +### Authentication Filter +```bash +wheels generate code auth-filter +``` + +Generates: +```cfc +function init() { + filters(through="authenticate", except="new,create"); +} + +private function authenticate() { + if (!StructKeyExists(session, "userId")) { + redirectTo(route="login"); + } +} +``` + +### Password Reset +```bash +wheels generate code password-reset +``` + +Generates: +```cfc +function requestReset() { + user = model("User").findOne(where="email='#params.email#'"); + if (IsObject(user)) { + token = Hash(CreateUUID()); + user.update(resetToken=token, resetExpiresAt=DateAdd("h", 1, Now())); + // Send email with token + } +} +``` + +### User Registration +```bash +wheels generate code user-registration +``` + +Generates: +```cfm +#startFormTag(action="create")# + #textField(objectName="user", property="firstName", label="First Name")# + #textField(objectName="user", property="email", label="Email")# + #passwordField(objectName="user", property="password", label="Password")# + #submitTag(value="Register")# +#endFormTag()# +``` + +## Model Patterns + +### Soft Delete +```bash +wheels generate code soft-delete +``` + +Generates: +```cfc +function init() { + property(name="deletedAt", sql="deleted_at"); + beforeDelete("softDelete"); +} + +private function softDelete() { + this.deletedAt = Now(); + this.save(validate=false, callbacks=false); + return false; +} +``` + +### Audit Trail +```bash +wheels generate code audit-trail +``` + +Generates: +```cfc +function init() { + property(name="createdBy", sql="created_by"); + property(name="updatedBy", sql="updated_by"); + beforeSave("setAuditFields"); +} + +private function setAuditFields() { + if (StructKeyExists(session, "userId")) { + if (this.isNew()) this.createdBy = session.userId; + this.updatedBy = session.userId; + } +} +``` + +### Sluggable +```bash +wheels generate code sluggable +``` + +Generates: +```cfc +function init() { + property(name="slug"); + beforeSave("generateSlug"); +} + +private function generateSlug() { + if (!len(this.slug) && len(this.title)) { + this.slug = lCase(reReplace(this.title, "[^a-zA-Z0-9]", "-", "all")); + } +} +``` + +### Versionable +```bash +wheels generate code versionable +``` + +Generates: +```cfc +function init() { + property(name="version", default=1); + beforeUpdate("incrementVersion"); +} + +private function incrementVersion() { + this.version = this.version + 1; +} +``` + +### Searchable +```bash +wheels generate code searchable +``` + +Generates: +```cfc +function search(required string query) { + return findAll(where="title LIKE '%#arguments.query#%' OR content LIKE '%#arguments.query#%'"); +} +``` + +## Controller Patterns + +### CRUD Actions +```bash +wheels generate code crud-actions +``` + +Generates: +```cfc +function index() { + users = model("User").findAll(); +} + +function show() { + user = model("User").findByKey(params.key); +} + +function create() { + user = model("User").create(params.user); + if (user.valid()) { + redirectTo(route="user", key=user.id); + } else { + renderView(action="new"); + } +} +``` + +### API Controller +```bash +wheels generate code api-controller +``` + +Generates: +```cfc +function init() { + provides("json"); +} + +function index() { + users = model("User").findAll(); + renderWith(data={users=users}); +} +``` + +### Nested Resource +```bash +wheels generate code nested-resource +``` + +Generates: +```cfc +function init() { + filters(through="findParent"); +} + +private function findParent() { + user = model("User").findByKey(params.userId); +} +``` + +### Admin Controller +```bash +wheels generate code admin-controller +``` + +Generates: +```cfc +function init() { + filters(through="requireAdmin"); +} + +private function requireAdmin() { + if (!currentUser().isAdmin()) { + redirectTo(route="home"); + } +} +``` + +## View Patterns + +### Form with Errors +```bash +wheels generate code form-with-errors +``` + +Generates: +```cfm +#errorMessagesFor("user")# + +#startFormTag(action="create")# + #textField(objectName="user", property="firstName", label="First Name")# + + #user.errors("firstName").get()# + + #submitTag(value="Submit")# +#endFormTag()# +``` + +### Pagination Links +```bash +wheels generate code pagination-links +``` + +Generates: +```cfm + + + +``` + +### Search Form +```bash +wheels generate code search-form +``` + +Generates: +```cfm +#startFormTag(method="get")# + #textField(name="q", value=params.q, placeholder="Search...")# + #submitTag(value="Search")# +#endFormTag()# +``` + +### AJAX Form +```bash +wheels generate code ajax-form +``` + +Generates: +```cfm +#startFormTag(action="create", id="userForm")# + #textField(objectName="user", property="name")# + #submitTag(value="Submit")# +#endFormTag()# + + +``` + +## Database Snippets + +### Migration Indexes +```bash +wheels generate code migration-indexes +``` + +Generates: +```cfc +t.index("email"); +t.index(["last_name", "first_name"]); +t.index("email", unique=true); +t.index("user_id"); +``` + +### Seed Data +```bash +wheels generate code seed-data +``` + +Generates: +```cfc +execute("INSERT INTO users (name, email) VALUES ('Admin', 'admin@example.com')"); +``` + +### Constraints +```bash +wheels generate code constraints +``` + +Generates: +```cfc +t.references("user_id", "users"); +t.references("category_id", "categories"); +``` + +## Custom Code Snippets + +### Create Custom Code Snippet +```bash +wheels generate code --create my-custom-snippet +``` + +This creates a directory structure in `app/snippets/my-custom-snippet/`: +``` +my-custom-snippet/ +└── template.txt +``` + +You can then edit the template file and use your custom code snippet: +```bash +wheels generate code my-custom-snippet +``` + +## Output Options + +### Output to Console (Default) +```bash +wheels generate code login-form +# or explicitly: +wheels generate code login-form --output=console +``` + +### Save to File +```bash +wheels generate code api-controller --output=file --path=app/controllers/Api.cfc +``` + +Use `--force` to overwrite existing files: +```bash +wheels generate code api-controller --output=file --path=app/controllers/Api.cfc --force +``` + +### Customization Options +```bash +wheels generate code [pattern] --customize +``` + +Shows available customization options for code snippets. + +## Filter by Category + +List code snippets from a specific category: +```bash +wheels generate code --list --category=model +wheels generate code --list --category=authentication +wheels generate code --list --category=controller +wheels generate code --list --category=view +wheels generate code --list --category=database +``` + +## Best Practices + +1. **Review generated code**: Customize for your needs +2. **Understand the patterns**: Don't blindly copy +3. **Keep snippets updated**: Maintain with framework updates +4. **Share useful patterns**: Contribute back to community +5. **Document customizations**: Note changes made +6. **Test generated code**: Ensure it works in your context +7. **Use consistent patterns**: Across your application + +## See Also + +- [wheels generate controller](controller.md) - Generate controllers +- [wheels generate model](model.md) - Generate models +- [wheels scaffold](scaffold.md) - Generate complete resources \ No newline at end of file diff --git a/docs/src/command-line-tools/commands/generate/snippets.md b/docs/src/command-line-tools/commands/generate/snippets.md index eebca75821..446498969e 100644 --- a/docs/src/command-line-tools/commands/generate/snippets.md +++ b/docs/src/command-line-tools/commands/generate/snippets.md @@ -1,500 +1,56 @@ # wheels generate snippets -Generate code snippets and boilerplate code for common Wheels patterns. +Copies template snippets to your application. ## Synopsis ```bash -wheels generate snippets [pattern] [options] -wheels g snippets [pattern] [options] +wheels generate snippets +wheels g snippets ``` ## Description -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. +The `wheels generate snippets` command copies snippet templates to your application's `/app/snippets` folder. This provides you with a collection of code snippet templates that you can use and customize for common Wheels patterns. ## Arguments -| Argument | Description | Default | -|----------|-------------|---------| -| `pattern` | Snippet pattern to generate | Shows available patterns | +None. ## Options -| Option | Description | Valid Values | Default | -|--------|-------------|--------------|---------| -| `--list` | List all available snippets | `true`/`false` | `false` | -| `--category` | Filter by category | `authentication`, `model`, `controller`, `view`, `database` | All categories | -| `--output` | Output format | `console`, `file` | `console` | -| `--path` | Output file path (required when output=file) | Any valid file path | | -| `--customize` | Show customization options | `true`/`false` | `false` | -| `--create` | Create custom snippet template | `true`/`false` | `false` | -| `--force` | Overwrite existing files | `true`/`false` | `false` | +None. -## Available Snippets +## Usage -### List All Snippets -```bash -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: - - soft-delete - Soft delete implementation - - audit-trail - Audit trail with timestamps - - sluggable - URL-friendly slugs - - versionable - Version tracking - - searchable - Full-text search - -Controller: - - crud-actions - Complete CRUD actions - - api-controller - JSON API controller - - nested-resource - Nested resource controller - - admin-controller - Admin area controller - -View: - - 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 - - -Next steps: - 1. Generate a snippet: wheels g snippets -``` - -## Authentication Snippets - -### Login Form -```bash -wheels generate snippets login-form -``` - -Generates: -```cfm -#startFormTag(action="create")# - #textField(objectName="user", property="email", label="Email")# - #passwordField(objectName="user", property="password", label="Password")# - #checkBox(objectName="user", property="rememberMe", label="Remember me")# - #submitTag(value="Login")# -#endFormTag()# -``` - -**Note**: This is a basic snippet. You can customize it by saving to a file and editing: -```bash -wheels generate snippets login-form --output=file --path=app/views/sessions/new.cfm -``` - -### Authentication Filter -```bash -wheels generate snippets auth-filter -``` - -Generates: -```cfc -function init() { - filters(through="authenticate", except="new,create"); -} - -private function authenticate() { - if (!StructKeyExists(session, "userId")) { - redirectTo(route="login"); - } -} -``` - -### Password Reset -```bash -wheels generate snippets password-reset -``` - -Generates: -```cfc -function requestReset() { - user = model("User").findOne(where="email='#params.email#'"); - if (IsObject(user)) { - token = Hash(CreateUUID()); - user.update(resetToken=token, resetExpiresAt=DateAdd("h", 1, Now())); - // Send email with token - } -} -``` - -### User Registration -```bash -wheels generate snippets user-registration -``` - -Generates: -```cfm -#startFormTag(action="create")# - #textField(objectName="user", property="firstName", label="First Name")# - #textField(objectName="user", property="email", label="Email")# - #passwordField(objectName="user", property="password", label="Password")# - #submitTag(value="Register")# -#endFormTag()# -``` - -## Model Patterns - -### Soft Delete -```bash -wheels generate snippets soft-delete -``` - -Generates: -```cfc -function init() { - property(name="deletedAt", sql="deleted_at"); - beforeDelete("softDelete"); -} - -private function softDelete() { - this.deletedAt = Now(); - this.save(validate=false, callbacks=false); - return false; -} -``` - -### Audit Trail -```bash -wheels generate snippets audit-trail -``` - -Generates: -```cfc -function init() { - property(name="createdBy", sql="created_by"); - property(name="updatedBy", sql="updated_by"); - beforeSave("setAuditFields"); -} - -private function setAuditFields() { - if (StructKeyExists(session, "userId")) { - if (this.isNew()) this.createdBy = session.userId; - this.updatedBy = session.userId; - } -} -``` - -### Sluggable -```bash -wheels generate snippets sluggable -``` - -Generates: -```cfc -function init() { - property(name="slug"); - beforeSave("generateSlug"); -} - -private function generateSlug() { - if (!len(this.slug) && len(this.title)) { - this.slug = lCase(reReplace(this.title, "[^a-zA-Z0-9]", "-", "all")); - } -} -``` - -### Versionable -```bash -wheels generate snippets versionable -``` - -Generates: -```cfc -function init() { - property(name="version", default=1); - beforeUpdate("incrementVersion"); -} - -private function incrementVersion() { - this.version = this.version + 1; -} -``` - -### Searchable -```bash -wheels generate snippets searchable -``` - -Generates: -```cfc -function search(required string query) { - return findAll(where="title LIKE '%#arguments.query#%' OR content LIKE '%#arguments.query#%'"); -} -``` - -## Controller Patterns - -### CRUD Actions -```bash -wheels generate snippets crud-actions -``` - -Generates: -```cfc -function index() { - users = model("User").findAll(); -} - -function show() { - user = model("User").findByKey(params.key); -} - -function create() { - user = model("User").create(params.user); - if (user.valid()) { - redirectTo(route="user", key=user.id); - } else { - renderView(action="new"); - } -} -``` +### Generate Snippets -### API Controller ```bash -wheels generate snippets api-controller +wheels generate snippets ``` -Generates: -```cfc -function init() { - provides("json"); -} - -function index() { - users = model("User").findAll(); - renderWith(data={users=users}); -} -``` - -### Nested Resource -```bash -wheels generate snippets nested-resource -``` - -Generates: -```cfc -function init() { - filters(through="findParent"); -} - -private function findParent() { - user = model("User").findByKey(params.userId); -} -``` - -### Admin Controller -```bash -wheels generate snippets admin-controller -``` - -Generates: -```cfc -function init() { - filters(through="requireAdmin"); -} - -private function requireAdmin() { - if (!currentUser().isAdmin()) { - redirectTo(route="home"); - } -} -``` - -## View Patterns - -### Form with Errors -```bash -wheels generate snippets form-with-errors -``` - -Generates: -```cfm -#errorMessagesFor("user")# - -#startFormTag(action="create")# - #textField(objectName="user", property="firstName", label="First Name")# - - #user.errors("firstName").get()# - - #submitTag(value="Submit")# -#endFormTag()# -``` - -### Pagination Links -```bash -wheels generate snippets pagination-links -``` - -Generates: -```cfm - - - -``` - -### Search Form -```bash -wheels generate snippets search-form -``` - -Generates: -```cfm -#startFormTag(method="get")# - #textField(name="q", value=params.q, placeholder="Search...")# - #submitTag(value="Search")# -#endFormTag()# -``` - -### AJAX Form -```bash -wheels generate snippets ajax-form -``` - -Generates: -```cfm -#startFormTag(action="create", id="userForm")# - #textField(objectName="user", property="name")# - #submitTag(value="Submit")# -#endFormTag()# - - -``` - -## Database Snippets - -### Migration Indexes -```bash -wheels generate snippets migration-indexes -``` - -Generates: -```cfc -t.index("email"); -t.index(["last_name", "first_name"]); -t.index("email", unique=true); -t.index("user_id"); -``` - -### Seed Data -```bash -wheels generate snippets seed-data -``` - -Generates: -```cfc -execute("INSERT INTO users (name, email) VALUES ('Admin', 'admin@example.com')"); -``` - -### Constraints -```bash -wheels generate snippets constraints -``` - -Generates: -```cfc -t.references("user_id", "users"); -t.references("category_id", "categories"); -``` - -## Custom Snippets - -### Create Custom Snippet -```bash -wheels generate snippets --create my-custom-snippet -``` - -This creates a directory structure in `app/snippets/my-custom-snippet/`: +Output: ``` -my-custom-snippet/ -└── template.txt +Starting snippet generation... +Snippet successfully generated in the /app/snippets folder. ``` -You can then edit the template file and use your custom snippet: -```bash -wheels generate snippets my-custom-snippet -``` +This command: +1. Validates that the `app` directory exists in your current location +2. Ensures snippet templates are copied to `/app/snippets` +3. Confirms successful generation -## Output Options +## Requirements -### Output to Console (Default) -```bash -wheels generate snippets login-form -# or explicitly: -wheels generate snippets login-form --output=console -``` - -### Save to File -```bash -wheels generate snippets api-controller --output=file --path=app/controllers/Api.cfc -``` +- Must be run from your application root directory (where the `app` folder is located) -Use `--force` to overwrite existing files: -```bash -wheels generate snippets api-controller --output=file --path=app/controllers/Api.cfc --force -``` +## Error Handling -### Customization Options -```bash -wheels generate snippets [pattern] --customize +If the command cannot find the `app` directory, it will display an error: ``` - -Shows available customization options for snippets. - -## Filter by Category - -List snippets from a specific category: -```bash -wheels generate snippets --list --category=model -wheels generate snippets --list --category=authentication -wheels generate snippets --list --category=controller -wheels generate snippets --list --category=view -wheels generate snippets --list --category=database +[/path/to/app] can't be found. Are you running this command from your application root? ``` -## Best Practices - -1. **Review generated code**: Customize for your needs -2. **Understand the patterns**: Don't blindly copy -3. **Keep snippets updated**: Maintain with framework updates -4. **Share useful patterns**: Contribute back to community -5. **Document customizations**: Note changes made -6. **Test generated code**: Ensure it works in your context -7. **Use consistent patterns**: Across your application - ## See Also - [wheels generate controller](controller.md) - Generate controllers From 7f532874d9be4c12c4f9a64224f4d17ebfb585b8 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 29 Jan 2026 11:08:11 +0500 Subject: [PATCH 029/405] commit: update wheels docs for CLI app-wizard command --- docs/src/command-line-tools/commands/generate/app-wizard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/command-line-tools/commands/generate/app-wizard.md b/docs/src/command-line-tools/commands/generate/app-wizard.md index 624f5e0b12..9af22ca375 100644 --- a/docs/src/command-line-tools/commands/generate/app-wizard.md +++ b/docs/src/command-line-tools/commands/generate/app-wizard.md @@ -33,7 +33,7 @@ The `wheels generate app-wizard` command provides an interactive, step-by-step w | `directory` | Directory to create app in | Valid directory path | `{current directory}/{name}` | | `reloadPassword` | Reload password for the app | Any string | `changeMe` | | `datasourceName` | Database datasource name | Valid datasource name | `{app name}` | -| `cfmlEngine` | CFML engine for server.json | `lucee`, `adobe`, `lucee6`, `lucee5`, `adobe2023`, etc. | `lucee` | +| `cfmlEngine` | CFML engine for server.json | `lucee5, lucee6, lucee7`, `adobe2018`, `adobe2021`, `adobe2023`, `adobe2025`, `boxlang`, etc. | `lucee` | | `useBootstrap` | Add Bootstrap to the app | `true`, `false` | `false` | | `setupH2` | Setup H2 database for development | `true`, `false` | `true` | | `init` | Initialize directory as a package | `true`, `false` | `false` | From f68465966fc0bda03c913f6daeb3a02537156194 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 29 Jan 2026 11:56:42 +0500 Subject: [PATCH 030/405] commit: update wheels cli templates for wheels g command --- cli/src/commands/wheels/generate/app.cfc | 12 ++-- .../commands/generate/app.md | 58 ++++++++++++------- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/cli/src/commands/wheels/generate/app.cfc b/cli/src/commands/wheels/generate/app.cfc index 7ee92fb2c6..90b8f835e8 100644 --- a/cli/src/commands/wheels/generate/app.cfc +++ b/cli/src/commands/wheels/generate/app.cfc @@ -43,11 +43,11 @@ component aliases="wheels g app" extends="../base" { // Map these shortcut names to the actual ForgeBox slugs variables.templateMap = { - 'Base' : 'wheels-base-template@BE', - 'Base@BE' : 'wheels-base-template@BE', - 'HelloWorld' : 'cfwheels-template-helloworld', - 'HelloDynamic': 'cfwheels-template-hellodynamic', - 'HelloPages' : 'cfwheels-template-hellopages' + 'WheelsBaseTemplate' : 'wheels-base-template@^3.0.0', + 'BleedingEdge' : 'wheels-base-template@BE', + 'WheelsTemplateHTMX' : 'cfwheels-template-htmx-alpine-simple', + 'WheelsStarterApp' : 'wheels-starter-app', + 'WheelsTodoMVCHTMX' : 'cfwheels-todomvc-htmx' }; return this; @@ -68,7 +68,7 @@ component aliases="wheels g app" extends="../base" { **/ function run( name = 'MyApp', - template = 'wheels-base-template@BE', + template = 'wheels-base-template@^3.0.0', directory, reloadPassword = '', datasourceName, diff --git a/docs/src/command-line-tools/commands/generate/app.md b/docs/src/command-line-tools/commands/generate/app.md index ea2d1a94d4..5bce9fafa4 100644 --- a/docs/src/command-line-tools/commands/generate/app.md +++ b/docs/src/command-line-tools/commands/generate/app.md @@ -16,7 +16,7 @@ wheels g app [name] [template] [directory] [options] This command supports multiple parameter formats: - **Positional parameters**: `wheels generate app blog` (most common) -- **Named parameters**: `name=value` (e.g., `name=blog`, `template=HelloWorld`) +- **Named parameters**: `name=value` (e.g., `name=blog`, `template=WheelsBaseTemplate`) - **Flag parameters**: `--flag` equals `flag=true` (e.g., `--useBootstrap` equals `useBootstrap=true`) **Parameter Mixing Rules:** @@ -24,7 +24,7 @@ This command supports multiple parameter formats: **ALLOWED:** - All positional: `wheels generate app blog` - All positional + flags: `wheels generate app blog --useBootstrap --init` -- All named: `name=blog template=HelloWorld --useBootstrap` +- All named: `name=blog template=WheelsBaseTemplate --useBootstrap` **NOT ALLOWED:** - Positional + named: `wheels generate app blog name=myapp` (causes error) @@ -40,7 +40,7 @@ The `wheels generate app` command creates a new Wheels application with a comple | Argument | Description | Default | |----------|-------------|---------| | `name` | Application name | `MyApp` | -| `template` | Template to use | `wheels-base-template@BE` | +| `template` | Template to use | `wheels-base-template@^3.0.0` | | `directory` | Target directory | `./{name}` | ## Options @@ -57,38 +57,54 @@ The `wheels generate app` command creates a new Wheels application with a comple ## Available Templates -### wheels-base-template@BE (Default) +### wheels-base-template@^3.0.0 (stable) ```bash wheels generate app myapp ``` +- Backend Edition default template +- Complete MVC structure with proven, production-ready defaults +- Sample code with minimal, predictable configuration +- H2 database setup by default + +### BleedingEdge +```bash +wheels generate app myapp BleedingEdge +``` - Backend Edition template - Complete MVC structure - Sample code and configuration - H2 database setup by default -### HelloWorld +### WheelsStarterApp ```bash -wheels generate app myapp HelloWorld +wheels generate app myapp WheelsStarterApp ``` -- Simple "Hello World" example -- One controller and view -- Great for learning - -### HelloDynamic +- Starter user management and authentication application built with Wheels 3.0 +- Demonstrates best practices for security, conventions, and MVC architecture +- Full authentication & authorization flow (registration, verification, RBAC, admin panel) +- Built-in security features: CSRF protection, audit logging, bcrypt passwords, role checks +- Modern, responsive UI using Bootstrap with Wheels helpers +- Multi-database support with easy setup via CommandBox (MySQL, PostgreSQL, MSSQL, Oracle, H2) + +### WheelsTemplateHTMX ```bash -wheels generate app myapp HelloDynamic +wheels generate app myapp WheelsTemplateHTMX ``` -- Dynamic content example -- Database interaction -- Form handling - -### HelloPages +- Blank starter application for Wheels +- Full MVC structure pre-configured +- htmx integrated for server-side AJAX interactions +- Alpine.js included for lightweight client-side interactivity +- simple.css bundled for clean, minimal styling +- Ready-to-extend layout with sample configuration + +### WheelsTodoMVCHTMX ```bash -wheels generate app myapp HelloPages +wheels generate app myapp WheelsTodoMVCHTMX ``` -- Static pages example -- Layout system -- Navigation structure +- Reference TodoMVC implementation built with CFWheels +- Uses HTMX for server-driven interactivity +- Demonstrates real-world MVC and CRUD patterns +- Quick setup using CommandBox, CFWheels CLI, and H2 ## Examples From bdc9f0a4b95ea67b2d37c095b10934a56ccc453d Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 29 Jan 2026 12:31:19 +0500 Subject: [PATCH 031/405] commit: update property command datatypes for CLI --- cli/src/commands/wheels/dbmigrate/create/column.cfc | 2 +- cli/src/commands/wheels/generate/property.cfc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/commands/wheels/dbmigrate/create/column.cfc b/cli/src/commands/wheels/dbmigrate/create/column.cfc index 4e9ecdb5f6..b3b3969961 100644 --- a/cli/src/commands/wheels/dbmigrate/create/column.cfc +++ b/cli/src/commands/wheels/dbmigrate/create/column.cfc @@ -50,7 +50,7 @@ arguments = reconstructArgs( argStruct = arguments, allowedValues = { - dataType= ["string", "text", "integer", "biginteger", "float", "boolean", "date", "time", "datetime", "timestamp", "binary"] + dataType: ["biginteger", "binary", "boolean", "date", "datetime", "decimal", "float", "integer", "string", "text", "time", "timestamp", "uuid"] } ); diff --git a/cli/src/commands/wheels/generate/property.cfc b/cli/src/commands/wheels/generate/property.cfc index 09d1c32129..10c586c4a2 100644 --- a/cli/src/commands/wheels/generate/property.cfc +++ b/cli/src/commands/wheels/generate/property.cfc @@ -58,7 +58,7 @@ component aliases='wheels g property' extends="../base" { arguments = reconstructArgs( argStruct=arguments, allowedValues={ - dataType: ["biginteger", "binary", "boolean", "date", "datetime", "decimal", "float", "integer", "string", "limit", "text", "time", "timestamp", "uuid"] + dataType: ["biginteger", "binary", "boolean", "date", "datetime", "decimal", "float", "integer", "string", "text", "time", "timestamp", "uuid"] } ); From eab1e645b696bef772268d97f051892180e3139c Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 29 Jan 2026 15:00:52 +0500 Subject: [PATCH 032/405] commit: update wheels cli command, fixed scaffold command --- cli/src/commands/wheels/generate/scaffold.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/commands/wheels/generate/scaffold.cfc b/cli/src/commands/wheels/generate/scaffold.cfc index 3841c4e8a7..66038b6553 100644 --- a/cli/src/commands/wheels/generate/scaffold.cfc +++ b/cli/src/commands/wheels/generate/scaffold.cfc @@ -38,7 +38,7 @@ component aliases="wheels g scaffold, wheels g resource, wheels generate resourc // Custom validation for properties parameter format (name:type,name2:type2) if (len(trim(arguments.properties))) { - var validTypes = ["biginteger", "binary", "boolean", "date", "datetime", "decimal", "float", "integer", "string", "limit", "text", "time", "timestamp", "uuid"]; + var validTypes = ["biginteger", "binary", "boolean", "date", "datetime", "decimal", "float", "integer", "string", "text", "time", "timestamp", "uuid"]; var properties = listToArray(arguments.properties, ","); var invalidTypes = []; From c9da6d01ed030f9aabf0768fd348276e5cc480a4 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 30 Jan 2026 12:13:12 +0500 Subject: [PATCH 033/405] commit: fixed code snippets messages and scaffold dbmigrate issue --- cli/src/commands/wheels/generate/code.cfc | 34 +++++++++---------- cli/src/commands/wheels/generate/scaffold.cfc | 4 +-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cli/src/commands/wheels/generate/code.cfc b/cli/src/commands/wheels/generate/code.cfc index 04982a6f5e..a5ac0bd624 100644 --- a/cli/src/commands/wheels/generate/code.cfc +++ b/cli/src/commands/wheels/generate/code.cfc @@ -21,7 +21,7 @@ component aliases="wheels g code" extends="../base" { * @force.hint Overwrite existing files */ function run( - required string pattern, + string pattern, boolean list = false, string category = "", string output = "console", @@ -53,8 +53,8 @@ component aliases="wheels g code" extends="../base" { if (!len(arguments.pattern)) { detailOutput.error("Pattern name is required"); - detailOutput.getPrint().line("Usage: wheels g code "); - detailOutput.getPrint().line("Run 'wheels g code --list' to see available patterns"); + detailOutput.output("Usage: wheels g code ", true); + detailOutput.output("Run 'wheels g code --list' to see available patterns", true); setExitCode(1); return; } @@ -62,7 +62,7 @@ component aliases="wheels g code" extends="../base" { var snippet = getSnippetByName(arguments.pattern); if (!structCount(snippet)) { detailOutput.error("Code snippet '#arguments.pattern#' not found"); - detailOutput.getPrint().line("Run 'wheels g code --list' to see available patterns"); + detailOutput.output("Run 'wheels g code --list' to see available patterns", true); setExitCode(1); return; } @@ -102,16 +102,16 @@ component aliases="wheels g code" extends="../base" { for (var cat in categoryOrder) { var key = lCase(cat); if (structKeyExists(categories, key)) { - detailOutput.getPrint().line(""); - detailOutput.getPrint().boldLine("#cat#:"); + detailOutput.line(); + detailOutput.getPrint().boldLine("#cat#:").toConsole(); for (var snippet in categories[key]) { - detailOutput.getPrint().line(" - #snippet.name# - #snippet.description#"); + detailOutput.output(" - #snippet.name# - #snippet.description#", true); } } } - detailOutput.getPrint().line(""); - detailOutput.getPrint().line(""); + detailOutput.line(); + detailOutput.line(); detailOutput.nextSteps([ "Generate a code snippet: wheels g code " ]); @@ -122,11 +122,11 @@ component aliases="wheels g code" extends="../base" { */ private function printSnippet(required struct snippet) { detailOutput.header("Generating Code Snippet: #arguments.snippet.name#"); - detailOutput.getPrint().line(""); + detailOutput.line(); var content = getSnippetContent(arguments.snippet); - detailOutput.getPrint().line(content); - detailOutput.getPrint().line(""); + detailOutput.output("#content#"); + detailOutput.line(); detailOutput.success("Code snippet '#arguments.snippet.name#' generated successfully!"); } @@ -140,7 +140,7 @@ component aliases="wheels g code" extends="../base" { if (fileExists(resolvedPath) && !arguments.force) { detailOutput.error("File already exists: #resolvedPath#"); - detailOutput.getPrint().line("Use --force to overwrite"); + detailOutput.output("Use --force to overwrite", true); setExitCode(1); return; } @@ -429,9 +429,9 @@ component aliases="wheels g code" extends="../base" { */ private function showCustomizationOptions() { detailOutput.header("Customization Options"); - detailOutput.getPrint().line("You can customize code snippets by:"); - detailOutput.getPrint().line(" 1. Creating custom code snippets with --create"); - detailOutput.getPrint().line(" 2. Saving code snippets to files with --output=file"); - detailOutput.getPrint().line(" 3. Filtering by category with --category"); + detailOutput.output("You can customize code snippets by:"); + detailOutput.output(" 1. Creating custom code snippets with --create"); + detailOutput.output(" 2. Saving code snippets to files with --output=file"); + detailOutput.output(" 3. Filtering by category with --category"); } } diff --git a/cli/src/commands/wheels/generate/scaffold.cfc b/cli/src/commands/wheels/generate/scaffold.cfc index 66038b6553..10312875b0 100644 --- a/cli/src/commands/wheels/generate/scaffold.cfc +++ b/cli/src/commands/wheels/generate/scaffold.cfc @@ -101,13 +101,13 @@ component aliases="wheels g scaffold, wheels g resource, wheels generate resourc // Run migrations if requested if (arguments.migrate) { detailOutput.invoke("dbmigrate"); - command('wheels dbmigrate up').run(); + command('wheels dbmigrate latest').run(); } else if (!arguments.api) { // Only ask to migrate in interactive mode try { if (confirm("Would you like to run migrations now? [y/n]")) { detailOutput.invoke("dbmigrate"); - command('wheels dbmigrate up').run(); + command('wheels dbmigrate latest').run(); } } catch (any e) { // Skip if non-interactive From bf699cc6b21aad2b23a07d430e0a1460ed2dee94 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 30 Jan 2026 16:26:51 +0500 Subject: [PATCH 034/405] commit: update fixes for settings command --- cli/src/commands/wheels/get/settings.cfc | 48 +++++++++++------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/cli/src/commands/wheels/get/settings.cfc b/cli/src/commands/wheels/get/settings.cfc index 0378a70349..ece7af5edd 100644 --- a/cli/src/commands/wheels/get/settings.cfc +++ b/cli/src/commands/wheels/get/settings.cfc @@ -207,39 +207,35 @@ component extends="../base" { private void function parseSettings(required string content, required struct settings) { // Parse set() calls in the settings file - local.pattern = 'set\s*\(\s*([^=]+)\s*=\s*([^)]+)\)'; + local.pattern = 'set\s*\(\s*([^=\s]+(?:\s*[^=\s]*)*)\s*=\s*([^)]+)\)'; local.matches = REMatchNoCase(local.pattern, arguments.content); for (local.match in local.matches) { try { // Extract key and value - local.parts = REFind(local.pattern, local.match, 1, true); + local.extractPattern = 'set\s*\(\s*([^=]+?)\s*=\s*([^)]+)\)'; + local.parts = REFindNoCase(local.extractPattern, local.match, 1, true); + if (local.parts.pos[1] > 0) { - local.assignment = Mid(local.match, local.parts.pos[2], local.parts.len[2]); - local.assignParts = ListToArray(local.assignment, "="); - if (ArrayLen(local.assignParts) >= 2) { - local.key = Trim(local.assignParts[1]); - // Join remaining parts with = in case value contains = - local.valueParts = []; - for (local.i = 2; local.i <= ArrayLen(local.assignParts); local.i++) { - ArrayAppend(local.valueParts, local.assignParts[local.i]); - } - local.value = Trim(ArrayToList(local.valueParts, "=")); - - // Clean up the value - local.value = REReplace(local.value, "^['""]|['""]$", "", "all"); - - // Try to parse boolean/numeric values - if (local.value == "true") { - local.value = true; - } else if (local.value == "false") { - local.value = false; - } else if (IsNumeric(local.value)) { - local.value = Val(local.value); - } - - arguments.settings[local.key] = local.value; + // Extract key (trim whitespace) + local.key = Trim(Mid(local.match, local.parts.pos[2], local.parts.len[2])); + + // Extract value (trim whitespace) + local.value = Trim(Mid(local.match, local.parts.pos[3], local.parts.len[3])); + + // Clean up quotes from the value + local.value = REReplace(local.value, "^['""]|['""]$", "", "all"); + + // Try to parse boolean/numeric values + if (local.value == "true") { + local.value = true; + } else if (local.value == "false") { + local.value = false; + } else if (IsNumeric(local.value)) { + local.value = Val(local.value); } + + arguments.settings[local.key] = local.value; } } catch (any e) { // Skip malformed settings From fdde56eec0793fbd8c4d5d579366fe4f0aae70fb Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Fri, 30 Jan 2026 17:21:29 +0500 Subject: [PATCH 035/405] commit: update wheels config dump output command --- cli/src/commands/wheels/config/dump.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/commands/wheels/config/dump.cfc b/cli/src/commands/wheels/config/dump.cfc index f72b339350..262435e7fe 100644 --- a/cli/src/commands/wheels/config/dump.cfc +++ b/cli/src/commands/wheels/config/dump.cfc @@ -88,7 +88,7 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { // Output to console if not table format if(arguments.format == "json"){ detailOutput.line(); - detailOutput.output(deserializeJSON(local.outputContent)); + detailOutput.getPrint().line(deserializeJSON(local.outputContent)).toConsole(); } else { detailOutput.line(); detailOutput.output(local.outputContent); From 8e6848038356570562a2030dfabb1d383a6b3e83 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 2 Feb 2026 11:20:44 +0500 Subject: [PATCH 036/405] commit: fixes for severity param for wheels cli code command --- cli/src/commands/wheels/analyze/code.cfc | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cli/src/commands/wheels/analyze/code.cfc b/cli/src/commands/wheels/analyze/code.cfc index 738314996a..d47368b410 100644 --- a/cli/src/commands/wheels/analyze/code.cfc +++ b/cli/src/commands/wheels/analyze/code.cfc @@ -180,6 +180,7 @@ component extends="../base" { // Display issues by file if (structCount(results.files) > 0) { detailOutput.subHeader("Issues by File"); + var filteredSeverity = lcase(arguments.severity); for (var filePath in results.files) { var fileIssues = results.files[filePath]; var relativePath = replace(filePath, getCWD(), ""); @@ -189,15 +190,13 @@ component extends="../base" { // Group issues by severity for better readability var groupedIssues = groupIssuesBySeverity(fileIssues); - for (var severity in ["error", "warning", "info"]) { - if (structKeyExists(groupedIssues, severity) && arrayLen(groupedIssues[severity]) > 0) { - for (var issue in groupedIssues[severity]) { - var icon = getSeverityIcon(issue.severity); - var color = getSeverityColor(issue.severity); + if (structKeyExists(groupedIssues, filteredSeverity) && arrayLen(groupedIssues[filteredSeverity]) > 0) { + for (var issue in groupedIssues[filteredSeverity]) { + var icon = getSeverityIcon(issue.severity); + var color = getSeverityColor(issue.severity); - detailOutput.output("#icon# Line #issue.line#:#issue.column# - #issue.message#", true); - print.cyanLine(" Rule: #issue.rule#" & (issue.fixable ? " [Auto-fixable]" : "")).toConsole(); - } + detailOutput.output("#icon# Line #issue.line#:#issue.column# - #issue.message#", true); + print.cyanLine(" Rule: #issue.rule#" & (issue.fixable ? " [Auto-fixable]" : "")).toConsole(); } } From 9349bf1365ae02f05b409129e4b950830f224289 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 2 Feb 2026 12:21:57 +0500 Subject: [PATCH 037/405] commit: fixes for wheels cli plugin commands. --- cli/src/commands/wheels/plugins/list.cfc | 2 +- cli/src/commands/wheels/plugins/outdated.cfc | 2 +- cli/src/commands/wheels/plugins/search.cfc | 2 +- cli/src/commands/wheels/plugins/update.cfc | 2 +- cli/src/commands/wheels/plugins/updateAll.cfc | 7 +++---- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cli/src/commands/wheels/plugins/list.cfc b/cli/src/commands/wheels/plugins/list.cfc index 953bd92643..b5ec92ca69 100644 --- a/cli/src/commands/wheels/plugins/list.cfc +++ b/cli/src/commands/wheels/plugins/list.cfc @@ -87,7 +87,7 @@ component aliases="wheels plugin list" extends="../base" { } // Display the table - detailOutput.getPrint().table(rows); + detailOutput.getPrint().table(rows).toConsole(); detailOutput.line(); detailOutput.divider("-", 60); diff --git a/cli/src/commands/wheels/plugins/outdated.cfc b/cli/src/commands/wheels/plugins/outdated.cfc index d91e7d92f6..f60f10c6b9 100644 --- a/cli/src/commands/wheels/plugins/outdated.cfc +++ b/cli/src/commands/wheels/plugins/outdated.cfc @@ -130,7 +130,7 @@ component aliases="wheels plugin outdated,wheels plugins outdated" extends="../b } // Display the table - print.table(rows); + print.table(rows).toConsole(); detailOutput.line(); detailOutput.divider("-", 60); diff --git a/cli/src/commands/wheels/plugins/search.cfc b/cli/src/commands/wheels/plugins/search.cfc index a2f6066b9d..56b04a6853 100644 --- a/cli/src/commands/wheels/plugins/search.cfc +++ b/cli/src/commands/wheels/plugins/search.cfc @@ -170,7 +170,7 @@ component aliases="wheels plugin search" extends="../base" { // Display the table - detailOutput.getPrint().table(rows); + detailOutput.getPrint().table(rows).toConsole(); detailOutput.line(); detailOutput.divider(); diff --git a/cli/src/commands/wheels/plugins/update.cfc b/cli/src/commands/wheels/plugins/update.cfc index 2a12eac6eb..a890505a22 100644 --- a/cli/src/commands/wheels/plugins/update.cfc +++ b/cli/src/commands/wheels/plugins/update.cfc @@ -196,7 +196,7 @@ component aliases="wheels plugin update" extends="../base" { // Show version comparison detailOutput.subHeader("Update Summary"); detailOutput.metric("Plugin", pluginInfo.name); - detailOutput.update("Version", "v#currentVersion# → v#targetVersion#"); + detailOutput.metric("Version", "v#currentVersion# → v#targetVersion#"); detailOutput.metric("Location", "/plugins/#foundPlugin.folderName#"); detailOutput.line(); diff --git a/cli/src/commands/wheels/plugins/updateAll.cfc b/cli/src/commands/wheels/plugins/updateAll.cfc index abac4c946d..92175b587b 100644 --- a/cli/src/commands/wheels/plugins/updateAll.cfc +++ b/cli/src/commands/wheels/plugins/updateAll.cfc @@ -4,7 +4,7 @@ * wheels plugin update:all * wheels plugin update:all --dryRun */ -component aliases="wheels plugin update:all,wheels plugins update:all" extends="../base" { +component aliases="wheels plugin update:all,wheels plugins update:all, wheels plugin updateall" extends="../base" { property name="pluginService" inject="PluginService@wheels-cli"; property name="packageService" inject="PackageService"; @@ -122,7 +122,7 @@ component aliases="wheels plugin update:all,wheels plugins update:all" extends=" }); } - print.table(updateRows); + print.table(updateRows).toConsole(); detailOutput.line(); if (arguments.dryRun) { @@ -198,7 +198,6 @@ component aliases="wheels plugin update:all,wheels plugins update:all" extends=" // Show final summary detailOutput.line(); - detailOutput.divider("=", 60); detailOutput.header("Update Summary"); detailOutput.line(); @@ -214,7 +213,7 @@ component aliases="wheels plugin update:all,wheels plugins update:all" extends=" arrayAppend(summaryRows, { "Status" = "Check errors", "Count" = "#arrayLen(errors)#" }); } - print.table(summaryRows); + print.table(summaryRows).toConsole(); detailOutput.line(); if (successCount > 0) { From 5001c3bdc92164b3ac847c5672ed8019807b8df3 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 2 Feb 2026 18:41:53 +0500 Subject: [PATCH 038/405] commit: update cli docs --- docs/src/command-line-tools/commands/test/test-run.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/src/command-line-tools/commands/test/test-run.md b/docs/src/command-line-tools/commands/test/test-run.md index b48a425e0e..60c5413805 100644 --- a/docs/src/command-line-tools/commands/test/test-run.md +++ b/docs/src/command-line-tools/commands/test/test-run.md @@ -454,7 +454,4 @@ box server restart ## See Also -- [wheels test](test.md) - Run framework tests -- [wheels test coverage](test-coverage.md) - Generate coverage -- [wheels test debug](test-debug.md) - Debug tests - [wheels generate test](../generate/test.md) - Generate test files \ No newline at end of file From f7f431870618c99f378aca6ccea4f687ed65198d Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Tue, 3 Feb 2026 11:51:20 +0500 Subject: [PATCH 039/405] commit: fixes for wheels cli command plugin list --- cli/src/commands/wheels/plugins/list.cfc | 122 ++++++++++++++++++++++- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/cli/src/commands/wheels/plugins/list.cfc b/cli/src/commands/wheels/plugins/list.cfc index b5ec92ca69..14d85e06fe 100644 --- a/cli/src/commands/wheels/plugins/list.cfc +++ b/cli/src/commands/wheels/plugins/list.cfc @@ -6,7 +6,7 @@ * wheels plugins list --available */ component aliases="wheels plugin list" extends="../base" { - + property name="forgebox" inject="ForgeBox"; property name="pluginService" inject="PluginService@wheels-cli"; property name="detailOutput" inject="DetailOutputService@wheels-cli"; @@ -30,8 +30,120 @@ component aliases="wheels plugin list" extends="../base" { if (arguments.available) { // Show available plugins from ForgeBox detailOutput.header("Available Wheels Plugins on ForgeBox"); + detailOutput.output("Searching, please wait..."); detailOutput.line(); - command('forgebox show').params(type="cfwheels-plugins").run(); + + // Get list of all cfwheels plugins slugs + var forgeboxResult = command('forgebox show') + .params(type='cfwheels-plugins') + .run(returnOutput=true); + + var results = []; + + if (len(forgeboxResult)) { + var lines = listToArray(forgeboxResult, chr(10) & chr(13)); + + for (var i = 1; i <= arrayLen(lines); i++) { + var line = trim(lines[i]); + + // Check if this is a slug line: Slug: "slug-name" + if (findNoCase('Slug:', line)) { + // Extract slug from quotes + var slugMatch = reFind('Slug:\s*"([^"]+)"', line, 1, true); + if (slugMatch.pos[1] > 0) { + var slug = mid(line, slugMatch.pos[2], slugMatch.len[2]); + + try { + var pluginInfo = forgebox.getEntry(slug); + + if (isStruct(pluginInfo) && structKeyExists(pluginInfo, "slug")) { + // Extract version from latestVersion structure + var version = "N/A"; + if (structKeyExists(pluginInfo, "latestVersion") && + isStruct(pluginInfo.latestVersion) && + structKeyExists(pluginInfo.latestVersion, "version")) { + version = pluginInfo.latestVersion.version; + } + + // Extract author from user structure + var author = "Unknown"; + if (structKeyExists(pluginInfo, "user") && + isStruct(pluginInfo.user) && + structKeyExists(pluginInfo.user, "username")) { + author = pluginInfo.user.username; + } + + arrayAppend(results, { + name: pluginInfo.title ?: slug, + slug: slug, + version: version, + description: pluginInfo.summary ?: pluginInfo.description ?: "", + author: author, + downloads: pluginInfo.hits ?: 0, + updateDate: pluginInfo.updatedDate ?: "" + }); + } + } catch (any e) { + // Skip plugins that can't be retrieved + } + } + } + } + } + + results.sort(function(a, b) { + return compareNoCase(a.name, b.name); + }); + + if (arguments.format == "json") { + var jsonOutput = { + "plugins": results, + "count": arrayLen(results) + }; + print.line(jsonOutput).toConsole(); + } else { + detailOutput.subHeader("Found #arrayLen(results)# plugin(s)"); + detailOutput.line(); + + // Create table for results + var rows = []; + + for (var plugin in results) { + // use ordered struct so JSON keeps key order + var row = structNew("ordered"); + + row["Name"] = plugin.name; + row["Slug"] = plugin.slug; + row["Version"] = plugin.version; + row["Downloads"] = numberFormat(plugin.downloads ?: 0); + row["Description"] = plugin.description ?: "No description"; + + // Truncate long descriptions + if (len(row["Description"]) > 50) { + row["Description"] = left(row["Description"], 47) & "..."; + } + + arrayAppend(rows, row); + } + + // Display the table + detailOutput.getPrint().table(rows).toConsole(); + + detailOutput.line(); + detailOutput.divider(); + detailOutput.line(); + + // Show summary + detailOutput.metric("Total plugins found", "#arrayLen(results)#"); + detailOutput.line(); + + // Show commands + detailOutput.subHeader("Commands"); + detailOutput.output("- Install: wheels plugin install ", true); + detailOutput.output("- Details: wheels plugin info ", true); + detailOutput.output("- Add --format=json for JSON output", true); + detailOutput.line(); + } return; } @@ -58,17 +170,17 @@ component aliases="wheels plugin list" extends="../base" { "plugins": plugins, "count": arrayLen(plugins) }; - print.line(serializeJSON(jsonOutput, true)); + print.line(jsonOutput).toConsole(); } else { // Table format output detailOutput.header("Installed Wheels Plugins (#arrayLen(plugins)#)"); - detailOutput.line(); // Create table rows var rows = []; for (var plugin in plugins) { var row = { "Plugin Name": plugin.name, + "Slug" :plugin.slug, "Version": plugin.version }; @@ -87,7 +199,7 @@ component aliases="wheels plugin list" extends="../base" { } // Display the table - detailOutput.getPrint().table(rows).toConsole(); + detailOutput.getPrint().table(rows); detailOutput.line(); detailOutput.divider("-", 60); From 2f3cf3cdf67b761faebdac3e2973feedb6dae36a Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Wed, 4 Feb 2026 15:26:26 +0500 Subject: [PATCH 040/405] update wirebox version --- tools/docker/boxlang/box.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/docker/boxlang/box.json b/tools/docker/boxlang/box.json index f1f598dae0..153980ba66 100644 --- a/tools/docker/boxlang/box.json +++ b/tools/docker/boxlang/box.json @@ -2,7 +2,7 @@ "name": "wheels-test-suite-boxlang", "version": "1.0.0", "dependencies": { - "wirebox": "^7.0.0", + "wirebox": "^8.0.0", "testbox": "^6.0.0", "bx-compat-cfml":"^1.27.0+35", "bx-csrf":"^1.2.0+3", From 5c57da24f5da67d5e0de9946bdebeba05bc997a9 Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Wed, 4 Feb 2026 16:45:12 +0500 Subject: [PATCH 041/405] update boxlang docker image tag --- tools/docker/boxlang/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/docker/boxlang/Dockerfile b/tools/docker/boxlang/Dockerfile index d38b6f214a..2d55b6e8bd 100644 --- a/tools/docker/boxlang/Dockerfile +++ b/tools/docker/boxlang/Dockerfile @@ -1,4 +1,4 @@ -FROM ortussolutions/commandbox:boxlang +FROM ortussolutions/commandbox:latest LABEL maintainer "Wheels Core Team" From 7f28c1c0bef21e54a3866e5839c70db65a1426bb Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Wed, 4 Feb 2026 16:48:20 +0500 Subject: [PATCH 042/405] update max minor version for boxlang --- core/src/wheels/Global.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/wheels/Global.cfc b/core/src/wheels/Global.cfc index 8d957a848c..557876505a 100644 --- a/core/src/wheels/Global.cfc +++ b/core/src/wheels/Global.cfc @@ -1557,7 +1557,7 @@ component output="false" { local.minimumMinor = "0"; local.minimumPatch = "0"; local.maximumMajor = "1"; - local.maximumMinor = "9"; + local.maximumMinor = "15"; local.maximumPatch = "999"; // Check minimum version From 88bace747e11f9aca282ddc8c4724302d8472295 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 4 Feb 2026 17:32:03 +0500 Subject: [PATCH 043/405] commit: update fixes for boxlang compatibility --- core/src/wheels/databaseAdapters/Base.cfc | 2 ++ core/src/wheels/tests_testbox/runner.cfm | 2 +- templates/base/src/tests/runner.cfm | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/wheels/databaseAdapters/Base.cfc b/core/src/wheels/databaseAdapters/Base.cfc index e922bfa4ea..7cc7822703 100755 --- a/core/src/wheels/databaseAdapters/Base.cfc +++ b/core/src/wheels/databaseAdapters/Base.cfc @@ -112,6 +112,8 @@ component output=false extends="wheels.Global"{ ); if (structKeyExists(wheels,"id") && isStruct(wheels.id) && !structIsEmpty(wheels.id)) { + // BoxLang-safe: ensure modifiable + wheels.result = duplicate(wheels.result); structAppend(wheels.result, wheels.id); } diff --git a/core/src/wheels/tests_testbox/runner.cfm b/core/src/wheels/tests_testbox/runner.cfm index 1ad6f19495..145941100a 100644 --- a/core/src/wheels/tests_testbox/runner.cfm +++ b/core/src/wheels/tests_testbox/runner.cfm @@ -206,7 +206,7 @@ local.tableList = ValueList(local.tables.table_name) local.populate = StructKeyExists(url, "populate") ? url.populate : true if (local.populate || !FindNoCase("c_o_r_e_authors", local.tableList)) { - include "populate.cfm" + include "/wheels/tests_testbox/populate.cfm" } } diff --git a/templates/base/src/tests/runner.cfm b/templates/base/src/tests/runner.cfm index eb4a489ad2..1598d64c08 100644 --- a/templates/base/src/tests/runner.cfm +++ b/templates/base/src/tests/runner.cfm @@ -319,7 +319,7 @@ local.populate = StructKeyExists(url, "populate") ? url.populate : true if (local.populate) { - include "populate.cfm" + include "/tests/populate.cfm" } } From 89a81d8f579b93105c37047dcbd5886d9b013258 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 5 Feb 2026 12:28:05 +0500 Subject: [PATCH 044/405] commit: update docker command to move modules --- tools/docker/boxlang/Dockerfile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/docker/boxlang/Dockerfile b/tools/docker/boxlang/Dockerfile index 2d55b6e8bd..b4e824f1e7 100644 --- a/tools/docker/boxlang/Dockerfile +++ b/tools/docker/boxlang/Dockerfile @@ -30,5 +30,16 @@ COPY tools/docker/boxlang/settings.cfm ${APP_DIR}/config/settings.cfm # Install dependencies RUN box install +# Start once to create BoxLang engine +RUN box server start --background && sleep 10 && box server stop + +# Move bx-* modules if engine modules dir exists +RUN if [ -d ".engine/boxlang/WEB-INF/boxlang/modules" ]; then \ + echo "Moving bx-* modules"; \ + mv bx-* .engine/boxlang/WEB-INF/boxlang/modules/ 2>/dev/null || true; \ + else \ + echo "Engine modules directory missing"; \ + fi + # WARM UP THE SERVER RUN ${BUILD_DIR}/util/warmup-server.sh From 39d7267322313275a3df8112a7386ff5887dccbc Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 9 Feb 2026 13:05:20 +0500 Subject: [PATCH 045/405] commit: fixes for wheels starter app --- examples/starter-app/README.md | 15 +- examples/starter-app/app/global/functions.cfm | 16 +- ...0180519105946_Adds_Default_Permissions.cfc | 179 +++++++++++------- 3 files changed, 119 insertions(+), 91 deletions(-) diff --git a/examples/starter-app/README.md b/examples/starter-app/README.md index 9dde802cd5..1b7424516c 100644 --- a/examples/starter-app/README.md +++ b/examples/starter-app/README.md @@ -6,10 +6,6 @@ This repository contains a **user management and authentication web application* **Important**: This is **not a complete, full-featured app**, but rather a **starter/example app** built with Wheels 3.0. It is designed to help you get started and to showcase best practices in authentication, authorization, auditing, and modern web UI using Wheels. -## Installation - -See [Installation](https://github.com/wheels-dev/Wheels-example-app/wiki/Installation) - ## Key Features ### User Registration & Verification @@ -99,6 +95,7 @@ app/ │ ├── PasswordResets.cfc # Password management │ ├── Accounts.cfc # User account management │ └── admin/ # Admin controllers +│ └── functions/ # helper functions ├── models/ # Data models │ ├── User.cfc # User model with authentication │ ├── Role.cfc # Role model @@ -107,7 +104,7 @@ app/ |── global/ # application-wide globally accessible functions ├── views/ # Presentation layer ├── mailers/ # Email templates -└── plugins/ # Third-party plugins +plugins/ # Third-party plugins ``` ### Key Design Principles @@ -122,8 +119,8 @@ app/ ### Backend - **3.0.0-snapshot** - MVC Framework -- **Lucee 5, Lucee 6** - CFML Engine -- **Database** - MySQL, PostgreSQL, Microsoft SQL Server, Oracle, H2 +- **Lucee 5,6,7, Adobe 2018-2025, Boxlang** - CFML Engine +- **Database** - MySQL, PostgreSQL, Microsoft SQL Server, Oracle, SQLite, H2 - **WireBox** - Dependency injection - **TestBox** - Testing framework @@ -144,13 +141,15 @@ app/ - **CommandBox** - Latest version - **CFML Engine**: Choose one of the following: - - Adobe ColdFusion 2018/2021/2023 + - Adobe ColdFusion 2018/2021/2023/2025 - Lucee 5, Lucee 6, Lucee 7 + - Boxlang - **Database Engine**: Choose one of the following: - MySQL - PostgreSQL - Microsoft SQL Server - Oracle Database + - SQLite Database - H2 Database (for development/testing) ### Environment Configuration diff --git a/examples/starter-app/app/global/functions.cfm b/examples/starter-app/app/global/functions.cfm index cca83818e4..19165619f2 100644 --- a/examples/starter-app/app/global/functions.cfm +++ b/examples/starter-app/app/global/functions.cfm @@ -2,16 +2,8 @@ //===================================================================== //= Global Functions //===================================================================== - if (StructKeyExists(server, "lucee")) { - include "install.cfm"; - include "auth.cfm"; - include "logging.cfm"; - include "utils.cfm"; - } else { - // TODO: Check this doesn't break when in a subdir? - include "/global/install.cfm"; - include "/global/auth.cfm"; - include "/global/logging.cfm"; - include "/global/utils.cfm"; - } + include "install.cfm"; + include "auth.cfm"; + include "logging.cfm"; + include "utils.cfm"; diff --git a/examples/starter-app/app/migrator/migrations/20180519105946_Adds_Default_Permissions.cfc b/examples/starter-app/app/migrator/migrations/20180519105946_Adds_Default_Permissions.cfc index bab00d85f9..875b039789 100644 --- a/examples/starter-app/app/migrator/migrations/20180519105946_Adds_Default_Permissions.cfc +++ b/examples/starter-app/app/migrator/migrations/20180519105946_Adds_Default_Permissions.cfc @@ -28,77 +28,114 @@ component extends="wheels.migrator.Migration" hint="Adds Default Permissions" { try { c=0; - addRecord(table='permissions', id=++c, name='admin', description='Global Administrative Access'); - addRecord(table='rolepermissions', roleid=1, permissionid=c) - - addRecord(table='permissions', id=++c, name='admin.auditlogs', description='Allow Global Administrative Access to Logs'); - addRecord(table='permissions', id=++c, name='admin.auditlogs.index', description='View Logs'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.auditlogs.show', description='Show Log Extended Data') - addRecord(table='permissions', id=++c, name='admin.permissions', description='Allow Global Administrative Access to Permissions'); - addRecord(table='permissions', id=++c, name='admin.permissions.index', description='List Permissions'); - addRecord(table='permissions', id=++c, name='admin.permissions.edit', description='Edit Permission'); - addRecord(table='permissions', id=++c, name='admin.permissions.update', description='Update Permission') - addRecord(table='permissions', id=++c, name='admin.settings', description='Allow Global Administrative Access to Settings'); - addRecord(table='permissions', id=++c, name='admin.settings.index', description='List Settings'); - addRecord(table='permissions', id=++c, name='admin.settings.edit', description='Edit Setting'); - addRecord(table='permissions', id=++c, name='admin.settings.update', description='Update Setting') - addRecord(table='permissions', id=++c, name='admin.users', description='Allow Global Administrative Access to Users'); - - addRecord(table='permissions', id=++c, name='admin.users.index', description='List Users'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.new', description='New User'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.create', description='Create User'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.edit', description='Edit User'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.update', description='Update User'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.delete', description='Delete User'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.reset', description='Reset Users Password'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.recover', description='Recover User'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.show', description='View User'); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - - addRecord(table='permissions', id=++c, name='admin.users.assume', description='Assume Users (Grant only to Admins)'); - addRecord(table='permissions', id=++c, name='admin.users.destroy', description='Destroy Users (Grant only to Admins)'); - addRecord(table='permissions', id=++c, name='admin.roles', description='Allow Global Administrative Access to Roles'); - addRecord(table='permissions', id=++c, name='admin.roles.index', description='List Roles'); - addRecord(table='permissions', id=++c, name='admin.roles.new', description='New Role'); - addRecord(table='permissions', id=++c, name='admin.roles.create', description='Create Role'); - addRecord(table='permissions', id=++c, name='admin.roles.edit', description='Edit Role'); - addRecord(table='permissions', id=++c, name='admin.roles.update', description='Update Role'); - addRecord(table='permissions', id=++c, name='admin.roles.delete', description='Delete Role'); - - addRecord(table='permissions', id=++c, name='accounts', description='Allow Global Access to Own Profile'); - addRecord(table='rolepermissions', roleid=1, permissionid=c); - addRecord(table='rolepermissions', roleid=2, permissionid=c); - addRecord(table='rolepermissions', roleid=3, permissionid=c); - - addRecord(table='permissions', id=++c, name='accounts.show', description='View My Account'); - addRecord(table='permissions', id=++c, name='accounts.edit', description='Edit Own Account'); - addRecord(table='permissions', id=++c, name='accounts.update', description='Update Own Account'); - - /* - Named Permissions : arbitary permissions - */ - addRecord(table='permissions', id=++c, name='canViewAdminNotes', type="named", description='Allow user to view admin notes'); - - addRecord(table='permissions', id=++c, name='canViewLogData', type="named", description='Allow user to view extended log data'); - addRecord(table='rolepermissions', roleid=1, permissionid=c); + c++ + addRecord(table='permissions', id=c, name='admin', description='Global Administrative Access'); + addRecord(table='rolepermissions', roleid=1, permissionid=c) + + c++ + addRecord(table='permissions', id=c, name='admin.auditlogs', description='Allow Global Administrative Access to Logs'); + c++ + addRecord(table='permissions', id=c, name='admin.auditlogs.index', description='View Logs'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.auditlogs.show', description='Show Log Extended Data') + c++ + addRecord(table='permissions', id=c, name='admin.permissions', description='Allow Global Administrative Access to Permissions'); + c++ + addRecord(table='permissions', id=c, name='admin.permissions.index', description='List Permissions'); + c++ + addRecord(table='permissions', id=c, name='admin.permissions.edit', description='Edit Permission'); + c++ + addRecord(table='permissions', id=c, name='admin.permissions.update', description='Update Permission') + c++ + addRecord(table='permissions', id=c, name='admin.settings', description='Allow Global Administrative Access to Settings'); + c++ + addRecord(table='permissions', id=c, name='admin.settings.index', description='List Settings'); + c++ + addRecord(table='permissions', id=c, name='admin.settings.edit', description='Edit Setting'); + c++ + addRecord(table='permissions', id=c, name='admin.settings.update', description='Update Setting') + c++ + addRecord(table='permissions', id=c, name='admin.users', description='Allow Global Administrative Access to Users'); + + c++ + addRecord(table='permissions', id=c, name='admin.users.index', description='List Users'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.new', description='New User'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.create', description='Create User'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.edit', description='Edit User'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.update', description='Update User'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.delete', description='Delete User'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.reset', description='Reset Users Password'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.recover', description='Recover User'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.show', description='View User'); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='admin.users.assume', description='Assume Users (Grant only to Admins)'); + c++ + addRecord(table='permissions', id=c, name='admin.users.destroy', description='Destroy Users (Grant only to Admins)'); + c++ + addRecord(table='permissions', id=c, name='admin.roles', description='Allow Global Administrative Access to Roles'); + c++ + addRecord(table='permissions', id=c, name='admin.roles.index', description='List Roles'); + c++ + addRecord(table='permissions', id=c, name='admin.roles.new', description='New Role'); + c++ + addRecord(table='permissions', id=c, name='admin.roles.create', description='Create Role'); + c++ + addRecord(table='permissions', id=c, name='admin.roles.edit', description='Edit Role'); + c++ + addRecord(table='permissions', id=c, name='admin.roles.update', description='Update Role'); + c++ + addRecord(table='permissions', id=c, name='admin.roles.delete', description='Delete Role'); + + c++ + addRecord(table='permissions', id=c, name='accounts', description='Allow Global Access to Own Profile'); + addRecord(table='rolepermissions', roleid=1, permissionid=c); + addRecord(table='rolepermissions', roleid=2, permissionid=c); + addRecord(table='rolepermissions', roleid=3, permissionid=c); + + c++ + addRecord(table='permissions', id=c, name='accounts.show', description='View My Account'); + c++ + addRecord(table='permissions', id=c, name='accounts.edit', description='Edit Own Account'); + c++ + addRecord(table='permissions', id=c, name='accounts.update', description='Update Own Account'); + + /* + Named Permissions : arbitary permissions + */ + c++ + addRecord(table='permissions', id=c, name='canViewAdminNotes', type="named", description='Allow user to view admin notes'); + + c++ + addRecord(table='permissions', id=c, name='canViewLogData', type="named", description='Allow user to view extended log data'); + addRecord(table='rolepermissions', roleid=1, permissionid=c); } catch (any e) { local.exception = e; From 079951205e189c1092f70044a7c5b5b941e70270 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 9 Feb 2026 16:45:34 +0500 Subject: [PATCH 046/405] commit: update fixes for wheels starterapp and manual installation --- core/src/wheels/events/onapplicationstart.cfc | 4 ++-- examples/starter-app/box.json | 6 ++++-- examples/starter-app/public/Application.cfc | 8 ++++---- examples/tweet/public/Application.cfc | 2 +- tools/docker/boxlang/box.json | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/src/wheels/events/onapplicationstart.cfc b/core/src/wheels/events/onapplicationstart.cfc index ef04aba28e..94bc750ba0 100644 --- a/core/src/wheels/events/onapplicationstart.cfc +++ b/core/src/wheels/events/onapplicationstart.cfc @@ -283,8 +283,8 @@ component { application.$wheels.imagePath = "images"; application.$wheels.javascriptPath = "javascripts"; application.$wheels.modelPath = "/app/models"; - application.$wheels.pluginPath = "/plugins"; - application.$wheels.pluginComponentPath = "/plugins"; + application.$wheels.pluginPath = "plugins"; + application.$wheels.pluginComponentPath = "plugins"; application.$wheels.stylesheetPath = "stylesheets"; application.$wheels.viewPath = "/app/views"; application.$wheels.controllerPath = "/app/controllers"; diff --git a/examples/starter-app/box.json b/examples/starter-app/box.json index f9d5c51aa7..d6a0856e02 100644 --- a/examples/starter-app/box.json +++ b/examples/starter-app/box.json @@ -31,12 +31,14 @@ "dependencies": { "wirebox": "^7.0.0", "testbox": "^6.0.0", - "wheels-core": "^3.0.0" + "wheels-core": "^3.0.0", + "cfwheels-authenticateThis":"^1" }, "installPaths": { "wirebox": "vendor/wirebox/", "testbox": "vendor/testbox/", - "wheels-core": "vendor/wheels/" + "wheels-core": "vendor/wheels/", + "cfwheels-authenticateThis":"plugins/authenticateThis/" }, "private":false, "license":[ diff --git a/examples/starter-app/public/Application.cfc b/examples/starter-app/public/Application.cfc index 9ca29f84f4..cfe3a9b814 100644 --- a/examples/starter-app/public/Application.cfc +++ b/examples/starter-app/public/Application.cfc @@ -16,7 +16,6 @@ component output="false" { this.wheelsDir = this.vendorDir & "wheels/"; this.wireboxDir = this.vendorDir & "wirebox/"; this.testboxDir = this.vendorDir & "testbox/"; - // Set up the mappings for the application. this.mappings["/app"] = this.appDir; this.mappings["/vendor"] = this.vendorDir; @@ -25,12 +24,13 @@ component output="false" { this.mappings["/testbox"] = this.testboxDir; this.mappings["/tests"] = expandPath("../tests"); this.mappings["/config"] = expandPath("../config"); + this.mappings["/plugins"] = expandPath("../plugins"); // We turn on "sessionManagement" by default since the Flash uses it. this.sessionManagement = true; // If a plugin has a jar or class file, automatically add the mapping to this.javasettings. - this.wheels.pluginDir = this.appDir & "plugins"; + this.wheels.pluginDir = this.appDir & "../plugins"; this.wheels.pluginFolders = DirectoryList( this.wheels.pluginDir, "true", @@ -259,7 +259,7 @@ component output="false" { && StructKeyExists(application.wo, "$restoreTestRunnerApplicationScope") ) { application.wo.$restoreTestRunnerApplicationScope(); - application.wo.$include(template = "#application.wheels.eventPath#/onabort.cfm"); + application.wo.$include(template = "../../#application.wheels.eventPath#/onabort.cfm"); } return true; } @@ -312,7 +312,7 @@ component output="false" { location(url = local.redirectUrl, addToken = false); } - private string function $buildRedirectUrl() { + public string function $buildRedirectUrl() { // Determine the base URL if (StructKeyExists(cgi, "path_info") && Len(cgi.path_info)) { local.url = cgi.path_info; diff --git a/examples/tweet/public/Application.cfc b/examples/tweet/public/Application.cfc index 3006c33a90..cfe3a9b814 100755 --- a/examples/tweet/public/Application.cfc +++ b/examples/tweet/public/Application.cfc @@ -259,7 +259,7 @@ component output="false" { && StructKeyExists(application.wo, "$restoreTestRunnerApplicationScope") ) { application.wo.$restoreTestRunnerApplicationScope(); - application.wo.$include(template = "#application.wheels.eventPath#/onabort.cfm"); + application.wo.$include(template = "../../#application.wheels.eventPath#/onabort.cfm"); } return true; } diff --git a/tools/docker/boxlang/box.json b/tools/docker/boxlang/box.json index 153980ba66..f1f598dae0 100644 --- a/tools/docker/boxlang/box.json +++ b/tools/docker/boxlang/box.json @@ -2,7 +2,7 @@ "name": "wheels-test-suite-boxlang", "version": "1.0.0", "dependencies": { - "wirebox": "^8.0.0", + "wirebox": "^7.0.0", "testbox": "^6.0.0", "bx-compat-cfml":"^1.27.0+35", "bx-csrf":"^1.2.0+3", From 95fe7a81935807e87dba40a0c6f5c0dde6667fa2 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Tue, 10 Feb 2026 14:54:29 +0500 Subject: [PATCH 047/405] commit: update wheels snippets to load json files correctly --- cli/src/templates/BoxJSON.txt | 2 +- cli/src/templates/ServerJSON.txt | 10 ++++++---- .../command-line-tools/commands/docker/docker-init.md | 6 ++++++ examples/starter-app/app/snippets/BoxJSON.txt | 4 ++-- examples/starter-app/app/snippets/ServerJSON.txt | 10 ++++++---- examples/tweet/app/snippets/BoxJSON.txt | 4 ++-- examples/tweet/app/snippets/ServerJSON.txt | 10 ++++++---- templates/base/src/app/snippets/BoxJSON.txt | 4 ++-- templates/base/src/app/snippets/ServerJSON.txt | 10 ++++++---- 9 files changed, 37 insertions(+), 23 deletions(-) diff --git a/cli/src/templates/BoxJSON.txt b/cli/src/templates/BoxJSON.txt index 83a32bdf19..597b232073 100644 --- a/cli/src/templates/BoxJSON.txt +++ b/cli/src/templates/BoxJSON.txt @@ -4,7 +4,7 @@ "author":"Wheels Core Team and Community, repackaged by Peter Amiri", "shortDescription":"Wheels MVC Framework Base Template", "location":"", - "slug":"myapp", + "slug":"|appName|", "createPackageDirectory":false, "type":"wheels-templates", "keywords":[ diff --git a/cli/src/templates/ServerJSON.txt b/cli/src/templates/ServerJSON.txt index 2818423d36..5e33289071 100644 --- a/cli/src/templates/ServerJSON.txt +++ b/cli/src/templates/ServerJSON.txt @@ -1,14 +1,16 @@ { "name": "|appName|", - "app": { - "cfengine": "|setEngine|", - "serverHomeDirectory": ".wheels/server" - }, "web": { + "host":"localhost", "webroot": "public", "rewrites": { "enable": true, "config": "public/urlrewrite.xml" } + }, + "app": { + "cfengine": "|setEngine|", + "serverHomeDirectory": ".wheels/server", + "libDirs":"app/lib" } } \ No newline at end of file diff --git a/docs/src/command-line-tools/commands/docker/docker-init.md b/docs/src/command-line-tools/commands/docker/docker-init.md index 29d9d2c335..de56e0cdbd 100644 --- a/docs/src/command-line-tools/commands/docker/docker-init.md +++ b/docs/src/command-line-tools/commands/docker/docker-init.md @@ -595,7 +595,13 @@ The command automatically updates your `server.json` with Docker-specific settin "web": { "host": "0.0.0.0", "http": { + "enable": true, "port": "8080" + }, + "webroot":"public", + "rewrites":{ + "enable":true, + "config":"public/urlrewrite.xml" } }, "openBrowser": false, diff --git a/examples/starter-app/app/snippets/BoxJSON.txt b/examples/starter-app/app/snippets/BoxJSON.txt index 653ece14d4..ec30c9c8aa 100644 --- a/examples/starter-app/app/snippets/BoxJSON.txt +++ b/examples/starter-app/app/snippets/BoxJSON.txt @@ -4,7 +4,7 @@ "author":"Wheels Core Team and Community, repackaged by Peter Amiri", "shortDescription":"Wheels MVC Framework Base Template", "location":"", - "slug":"myapp", + "slug":"|appName|", "createPackageDirectory":false, "type":"wheels-templates", "keywords":[ @@ -35,7 +35,7 @@ }, "dependencies":{ "wheels":"|version|", - "orgh213172lex":"lex:https://ext.lucee.org/org.h2-1.3.172.lex" + "orgh213172lex":"lex:https://ext.lucee.org/org.lucee.h2-2.1.214.0001L.lex" }, "private":false, "license":[ diff --git a/examples/starter-app/app/snippets/ServerJSON.txt b/examples/starter-app/app/snippets/ServerJSON.txt index 2818423d36..5e33289071 100644 --- a/examples/starter-app/app/snippets/ServerJSON.txt +++ b/examples/starter-app/app/snippets/ServerJSON.txt @@ -1,14 +1,16 @@ { "name": "|appName|", - "app": { - "cfengine": "|setEngine|", - "serverHomeDirectory": ".wheels/server" - }, "web": { + "host":"localhost", "webroot": "public", "rewrites": { "enable": true, "config": "public/urlrewrite.xml" } + }, + "app": { + "cfengine": "|setEngine|", + "serverHomeDirectory": ".wheels/server", + "libDirs":"app/lib" } } \ No newline at end of file diff --git a/examples/tweet/app/snippets/BoxJSON.txt b/examples/tweet/app/snippets/BoxJSON.txt index 441a39b867..597b232073 100755 --- a/examples/tweet/app/snippets/BoxJSON.txt +++ b/examples/tweet/app/snippets/BoxJSON.txt @@ -4,7 +4,7 @@ "author":"Wheels Core Team and Community, repackaged by Peter Amiri", "shortDescription":"Wheels MVC Framework Base Template", "location":"", - "slug":"myapp", + "slug":"|appName|", "createPackageDirectory":false, "type":"wheels-templates", "keywords":[ @@ -35,7 +35,7 @@ }, "dependencies":{ "wheels":"|version|", - "orgh213172lex":"lex:https://ext.lucee.org/org.h2-1.3.172.lex" + "orgh213172lex":"lex:https://ext.lucee.org/org.lucee.h2-2.1.214.0001L.lex" }, "private":false, "license":[ diff --git a/examples/tweet/app/snippets/ServerJSON.txt b/examples/tweet/app/snippets/ServerJSON.txt index 2818423d36..5e33289071 100755 --- a/examples/tweet/app/snippets/ServerJSON.txt +++ b/examples/tweet/app/snippets/ServerJSON.txt @@ -1,14 +1,16 @@ { "name": "|appName|", - "app": { - "cfengine": "|setEngine|", - "serverHomeDirectory": ".wheels/server" - }, "web": { + "host":"localhost", "webroot": "public", "rewrites": { "enable": true, "config": "public/urlrewrite.xml" } + }, + "app": { + "cfengine": "|setEngine|", + "serverHomeDirectory": ".wheels/server", + "libDirs":"app/lib" } } \ No newline at end of file diff --git a/templates/base/src/app/snippets/BoxJSON.txt b/templates/base/src/app/snippets/BoxJSON.txt index 441a39b867..597b232073 100644 --- a/templates/base/src/app/snippets/BoxJSON.txt +++ b/templates/base/src/app/snippets/BoxJSON.txt @@ -4,7 +4,7 @@ "author":"Wheels Core Team and Community, repackaged by Peter Amiri", "shortDescription":"Wheels MVC Framework Base Template", "location":"", - "slug":"myapp", + "slug":"|appName|", "createPackageDirectory":false, "type":"wheels-templates", "keywords":[ @@ -35,7 +35,7 @@ }, "dependencies":{ "wheels":"|version|", - "orgh213172lex":"lex:https://ext.lucee.org/org.h2-1.3.172.lex" + "orgh213172lex":"lex:https://ext.lucee.org/org.lucee.h2-2.1.214.0001L.lex" }, "private":false, "license":[ diff --git a/templates/base/src/app/snippets/ServerJSON.txt b/templates/base/src/app/snippets/ServerJSON.txt index 2818423d36..5e33289071 100644 --- a/templates/base/src/app/snippets/ServerJSON.txt +++ b/templates/base/src/app/snippets/ServerJSON.txt @@ -1,14 +1,16 @@ { "name": "|appName|", - "app": { - "cfengine": "|setEngine|", - "serverHomeDirectory": ".wheels/server" - }, "web": { + "host":"localhost", "webroot": "public", "rewrites": { "enable": true, "config": "public/urlrewrite.xml" } + }, + "app": { + "cfengine": "|setEngine|", + "serverHomeDirectory": ".wheels/server", + "libDirs":"app/lib" } } \ No newline at end of file From 87a6769f9faa5a30f1425e52bad3dc61303228e9 Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Tue, 10 Feb 2026 15:11:49 +0500 Subject: [PATCH 048/405] Update test file Updated the html.cfm so that the user can run singular tests even when URL rewriting is turned off --- core/src/wheels/tests_testbox/html.cfm | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/core/src/wheels/tests_testbox/html.cfm b/core/src/wheels/tests_testbox/html.cfm index 5d054735fc..83e92cebef 100644 --- a/core/src/wheels/tests_testbox/html.cfm +++ b/core/src/wheels/tests_testbox/html.cfm @@ -153,6 +153,11 @@ + + + + +
#pageHeader(title="TestBox #type# Test Results")# @@ -193,8 +198,8 @@ - #result.cleanTestCase# - #result.cleanTestName# + #result.cleanTestCase# + #result.cleanTestName# #result.time# #result.status# @@ -205,8 +210,8 @@ - #result.cleanTestCase# - #result.cleanTestName# + #result.cleanTestCase# + #result.cleanTestName# #result.time# #result.status# @@ -232,8 +237,8 @@ - #result.cleanTestCase# - #result.cleanTestName# + #result.cleanTestCase# + #result.cleanTestName# #result.time# #result.status# @@ -249,7 +254,7 @@ #bundle.name#
Bundle has #bundle.totalError# error(s), but individual test details are not available in the TestBox results.
- Re-run this bundle to see detailed error information. + Re-run this bundle to see detailed error information.
@@ -272,8 +277,8 @@ - #result.cleanTestCase# - #result.cleanTestName# + #result.cleanTestCase# + #result.cleanTestName# #result.time# #result.status# @@ -281,8 +286,8 @@ - #result.cleanTestCase# - #result.cleanTestName# + #result.cleanTestCase# + #result.cleanTestName# #result.time# #result.status# From ad599b0ea3f3b95e8ac7c554e59fefa2aa19f968 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Wed, 11 Feb 2026 16:41:46 +0500 Subject: [PATCH 049/405] commit: applied try catch in cli commands --- cli/src/commands/wheels/assets/clean.cfc | 186 ++++----- cli/src/commands/wheels/assets/precompile.cfc | 183 ++++----- cli/src/commands/wheels/config/dump.cfc | 125 +++--- cli/src/commands/wheels/config/env.cfc | 65 +-- cli/src/commands/wheels/config/list.cfc | 129 +++--- cli/src/commands/wheels/config/set.cfc | 131 +++--- .../wheels/dbmigrate/create/blank.cfc | 53 +-- .../wheels/dbmigrate/create/column.cfc | 131 +++--- .../wheels/dbmigrate/create/table.cfc | 58 +-- cli/src/commands/wheels/dbmigrate/down.cfc | 108 ++--- cli/src/commands/wheels/dbmigrate/exec.cfc | 43 +- cli/src/commands/wheels/dbmigrate/info.cfc | 93 +++-- cli/src/commands/wheels/dbmigrate/latest.cfc | 81 ++-- .../wheels/dbmigrate/remove/table.cfc | 48 ++- cli/src/commands/wheels/dbmigrate/reset.cfc | 17 +- cli/src/commands/wheels/dbmigrate/up.cfc | 65 +-- cli/src/commands/wheels/deps.cfc | 88 ++-- cli/src/commands/wheels/destroy.cfc | 3 +- cli/src/commands/wheels/docs/generate.cfc | 205 +++++----- cli/src/commands/wheels/env/merge.cfc | 95 ++--- cli/src/commands/wheels/env/set.cfc | 38 +- cli/src/commands/wheels/init.cfc | 151 +++---- cli/src/commands/wheels/plugins/list.cfc | 375 +++++++++--------- cli/src/commands/wheels/reload.cfc | 55 +-- 24 files changed, 1323 insertions(+), 1203 deletions(-) diff --git a/cli/src/commands/wheels/assets/clean.cfc b/cli/src/commands/wheels/assets/clean.cfc index 75aba42d01..57c7706b3e 100644 --- a/cli/src/commands/wheels/assets/clean.cfc +++ b/cli/src/commands/wheels/assets/clean.cfc @@ -28,117 +28,121 @@ component aliases="clean" extends="../base" { numeric keep = 3, boolean dryRun = false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs( - argStruct=arguments, - numericRanges={ - keep:{min:1, max:100} - } - ); - var compiledDir = fileSystemUtil.resolvePath("public/assets/compiled"); + try{ + requireWheelsApp(getCWD()); + arguments = reconstructArgs( + argStruct=arguments, + numericRanges={ + keep:{min:1, max:100} + } + ); + var compiledDir = fileSystemUtil.resolvePath("public/assets/compiled"); - if (!directoryExists(compiledDir)) { - print.yellowLine("No compiled assets directory found. Nothing to clean.").toConsole(); - return; - } - - // Read current manifest - var manifestPath = compiledDir & "/manifest.json"; - var currentManifest = {}; - - if (fileExists(manifestPath)) { - try { - currentManifest = deserializeJSON(fileRead(manifestPath)); - } catch (any e) { - detailOutput.error("Error reading manifest file: #e.message#"); + if (!directoryExists(compiledDir)) { + print.yellowLine("No compiled assets directory found. Nothing to clean.").toConsole(); return; } - } - - detailOutput.line(); - if(!dryRun){ - print.greenBoldLine("Cleaning old compiled assets...").toConsole(); - }else{ - print.cyanBoldLine("Dry Running old compiled assets...").toConsole(); - } - detailOutput.line(); - - // Group files by base name - var fileGroups = {}; - var files = directoryList(compiledDir, false, "query", "*.*"); - - for (var file in files) { - if (file.type == "File" && file.name != "manifest.json") { - var baseName = extractBaseName(file.name); - if (!structKeyExists(fileGroups, baseName)) { - fileGroups[baseName] = []; + + // Read current manifest + var manifestPath = compiledDir & "/manifest.json"; + var currentManifest = {}; + + if (fileExists(manifestPath)) { + try { + currentManifest = deserializeJSON(fileRead(manifestPath)); + } catch (any e) { + detailOutput.error("Error reading manifest file: #e.message#"); + return; } - arrayAppend(fileGroups[baseName], { - name: file.name, - path: file.directory & "/" & file.name, - dateLastModified: file.dateLastModified - }); } - } - - var deletedCount = 0; - var freedSpace = 0; - // Process each group - for (var baseName in fileGroups) { - var group = fileGroups[baseName]; - - // Sort by date modified (newest first) - arraySort(group, function(a, b) { - return dateCompare(b.dateLastModified, a.dateLastModified); - }); + detailOutput.line(); + if(!dryRun){ + print.greenBoldLine("Cleaning old compiled assets...").toConsole(); + }else{ + print.cyanBoldLine("Dry Running old compiled assets...").toConsole(); + } + detailOutput.line(); - // Keep the specified number of versions (array is sorted newest first) - // Delete from the end of the array (oldest files) - if (arrayLen(group) > arguments.keep) { - if (arguments.dryRun) { - print.boldLine("Analyzing #baseName#...").toConsole(); - } else { - print.boldLine("Cleaning #baseName#...").toConsole(); + // Group files by base name + var fileGroups = {}; + var files = directoryList(compiledDir, false, "query", "*.*"); + + for (var file in files) { + if (file.type == "File" && file.name != "manifest.json") { + var baseName = extractBaseName(file.name); + if (!structKeyExists(fileGroups, baseName)) { + fileGroups[baseName] = []; + } + arrayAppend(fileGroups[baseName], { + name: file.name, + path: file.directory & "/" & file.name, + dateLastModified: file.dateLastModified + }); } + } - // Delete from the end (oldest) since array is sorted newest first - for (var i = arrayLen(group); i > arguments.keep; i--) { - var fileInfo = group[i]; - var fileSize = getFileInfo(fileInfo.path).size; + var deletedCount = 0; + var freedSpace = 0; + // Process each group + for (var baseName in fileGroups) { + var group = fileGroups[baseName]; + + // Sort by date modified (newest first) + arraySort(group, function(a, b) { + return dateCompare(b.dateLastModified, a.dateLastModified); + }); + + // Keep the specified number of versions (array is sorted newest first) + // Delete from the end of the array (oldest files) + if (arrayLen(group) > arguments.keep) { if (arguments.dryRun) { - detailOutput.output("Would delete: #fileInfo.name# (#formatFileSize(fileSize)#)"); - deletedCount++; - freedSpace += fileSize; + print.boldLine("Analyzing #baseName#...").toConsole(); } else { - try { - fileDelete(fileInfo.path); - print.redLine("Deleted: #fileInfo.name# (#formatFileSize(fileSize)#)").toConsole(); + print.boldLine("Cleaning #baseName#...").toConsole(); + } + + // Delete from the end (oldest) since array is sorted newest first + for (var i = arrayLen(group); i > arguments.keep; i--) { + var fileInfo = group[i]; + var fileSize = getFileInfo(fileInfo.path).size; + + if (arguments.dryRun) { + detailOutput.output("Would delete: #fileInfo.name# (#formatFileSize(fileSize)#)"); deletedCount++; freedSpace += fileSize; - } catch (any e) { - detailOutput.error("Error deleting #fileInfo.name#: #e.message#"); + } else { + try { + fileDelete(fileInfo.path); + print.redLine("Deleted: #fileInfo.name# (#formatFileSize(fileSize)#)").toConsole(); + deletedCount++; + freedSpace += fileSize; + } catch (any e) { + detailOutput.error("Error deleting #fileInfo.name#: #e.message#"); + } } } } } - } - - detailOutput.line(); + detailOutput.line(); - if (arguments.dryRun) { - print.yellowLine("Dry run complete. No files were deleted.").toConsole(); - detailOutput.output("Would delete #deletedCount# files"); - detailOutput.output("Would free #formatFileSize(freedSpace)# of disk space"); - } else if (deletedCount > 0) { - detailOutput.success("Asset cleaning complete!"); - print.greenLine("Deleted #deletedCount# old asset files").toConsole(); - print.greenLine("Freed #formatFileSize(freedSpace)# of disk space").toConsole(); - } else { - detailOutput.statusWarning("No old assets found to clean."); - } + if (arguments.dryRun) { + print.yellowLine("Dry run complete. No files were deleted.").toConsole(); + detailOutput.output("Would delete #deletedCount# files"); + detailOutput.output("Would free #formatFileSize(freedSpace)# of disk space"); + } else if (deletedCount > 0) { + detailOutput.success("Asset cleaning complete!"); + print.greenLine("Deleted #deletedCount# old asset files").toConsole(); + print.greenLine("Freed #formatFileSize(freedSpace)# of disk space").toConsole(); + } else { + detailOutput.statusWarning("No old assets found to clean."); + } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } /** diff --git a/cli/src/commands/wheels/assets/precompile.cfc b/cli/src/commands/wheels/assets/precompile.cfc index 4eccc97360..287f2422d0 100644 --- a/cli/src/commands/wheels/assets/precompile.cfc +++ b/cli/src/commands/wheels/assets/precompile.cfc @@ -38,106 +38,111 @@ component extends="../base" { boolean force = false, string environment = "production" ) { - requireWheelsApp(getCWD()); - // Reconstruct arguments for handling --prefixed options - arguments = reconstructArgs( - argStruct = arguments, - allowedValues = { - environment: ["production", "staging", "development", "test", "maintenance", "prod", "dev", "stage"] - } - ); + try{ + requireWheelsApp(getCWD()); + // Reconstruct arguments for handling --prefixed options + arguments = reconstructArgs( + argStruct = arguments, + allowedValues = { + environment: ["production", "staging", "development", "test", "maintenance", "prod", "dev", "stage"] + } + ); - // Normalize environment aliases - arguments.environment = normalizeEnvironment(arguments.environment); + // Normalize environment aliases + arguments.environment = normalizeEnvironment(arguments.environment); - detailOutput.output("Precompiling assets for #arguments.environment#..."); - detailOutput.line(); - - // Define asset directories - var publicDir = fileSystemUtil.resolvePath("public"); + detailOutput.output("Precompiling assets for #arguments.environment#..."); + detailOutput.line(); + + // Define asset directories + var publicDir = fileSystemUtil.resolvePath("public"); - var assetsDir = publicDir & "assets"; - var jsDir = publicDir & "javascripts"; - var cssDir = publicDir & "stylesheets"; - var imagesDir = publicDir & "images"; - - // Create compiled assets directory - var compiledDir = assetsDir & "/compiled"; - if (!directoryExists(compiledDir)) { - directoryCreate(compiledDir); - detailOutput.output("Created compiled assets directory: #compiledDir#"); - } - - // Initialize manifest - var manifest = {}; - var processedCount = 0; - - // Process JavaScript files - if (directoryExists(jsDir)) { - detailOutput.output("Processing JavaScript files..."); - var jsFiles = directoryList(jsDir, true, "query", "*.js"); - for (var file in jsFiles) { - if (file.type == "File" && !findNoCase(".min.js", file.name)) { - processedCount += processJavaScriptFile( - source = file.directory & "/" & file.name, - target = compiledDir, - manifest = manifest, - force = arguments.force, - environment = arguments.environment - ); + var assetsDir = publicDir & "assets"; + var jsDir = publicDir & "javascripts"; + var cssDir = publicDir & "stylesheets"; + var imagesDir = publicDir & "images"; + + // Create compiled assets directory + var compiledDir = assetsDir & "/compiled"; + if (!directoryExists(compiledDir)) { + directoryCreate(compiledDir); + detailOutput.output("Created compiled assets directory: #compiledDir#"); + } + + // Initialize manifest + var manifest = {}; + var processedCount = 0; + + // Process JavaScript files + if (directoryExists(jsDir)) { + detailOutput.output("Processing JavaScript files..."); + var jsFiles = directoryList(jsDir, true, "query", "*.js"); + for (var file in jsFiles) { + if (file.type == "File" && !findNoCase(".min.js", file.name)) { + processedCount += processJavaScriptFile( + source = file.directory & "/" & file.name, + target = compiledDir, + manifest = manifest, + force = arguments.force, + environment = arguments.environment + ); + } } } - } - - // Process CSS files - if (directoryExists(cssDir)) { - detailOutput.output("Processing CSS files..."); - var cssFiles = directoryList(cssDir, true, "query", "*.css"); - for (var file in cssFiles) { - if (file.type == "File" && !findNoCase(".min.css", file.name)) { - processedCount += processCSSFile( - source = file.directory & "/" & file.name, - target = compiledDir, - manifest = manifest, - force = arguments.force, - environment = arguments.environment - ); + + // Process CSS files + if (directoryExists(cssDir)) { + detailOutput.output("Processing CSS files..."); + var cssFiles = directoryList(cssDir, true, "query", "*.css"); + for (var file in cssFiles) { + if (file.type == "File" && !findNoCase(".min.css", file.name)) { + processedCount += processCSSFile( + source = file.directory & "/" & file.name, + target = compiledDir, + manifest = manifest, + force = arguments.force, + environment = arguments.environment + ); + } } } - } - - // Process image files - if (directoryExists(imagesDir)) { - detailOutput.output("Processing image files..."); - var imageFiles = directoryList(imagesDir, true, "query"); - for (var file in imageFiles) { - if (file.type == "File" && isImageFile(file.name)) { - processedCount += processImageFile( - source = file.directory & "/" & file.name, - target = compiledDir, - manifest = manifest, - force = arguments.force - ); + + // Process image files + if (directoryExists(imagesDir)) { + detailOutput.output("Processing image files..."); + var imageFiles = directoryList(imagesDir, true, "query"); + for (var file in imageFiles) { + if (file.type == "File" && isImageFile(file.name)) { + processedCount += processImageFile( + source = file.directory & "/" & file.name, + target = compiledDir, + manifest = manifest, + force = arguments.force + ); + } } } - } - - // Write manifest file - var manifestPath = compiledDir & "/manifest.json"; - fileWrite(manifestPath, serializeJSON(manifest)); - detailOutput.output("Asset manifest written to: #manifestPath#"); + + // Write manifest file + var manifestPath = compiledDir & "/manifest.json"; + fileWrite(manifestPath, serializeJSON(manifest)); + detailOutput.output("Asset manifest written to: #manifestPath#"); - detailOutput.line(); - detailOutput.statusSuccess("Asset precompilation complete!"); - detailOutput.output("Processed #processedCount# files", true); - detailOutput.output("Compiled assets location: #compiledDir#", true); + detailOutput.line(); + detailOutput.statusSuccess("Asset precompilation complete!"); + detailOutput.output("Processed #processedCount# files", true); + detailOutput.output("Compiled assets location: #compiledDir#", true); - // Provide instructions for production - detailOutput.line(); - detailOutput.output("To use precompiled assets in production:"); - detailOutput.output("1. Configure your web server to serve static files from /public/assets/compiled", true); - detailOutput.output("2. Update your application to use the asset manifest for cache-busted URLs", true); - detailOutput.output("3. Set wheels.assetManifest = true in your production environment", true); + // Provide instructions for production + detailOutput.line(); + detailOutput.output("To use precompiled assets in production:"); + detailOutput.output("1. Configure your web server to serve static files from /public/assets/compiled", true); + detailOutput.output("2. Update your application to use the asset manifest for cache-busted URLs", true); + detailOutput.output("3. Set wheels.assetManifest = true in your production environment", true); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } /** diff --git a/cli/src/commands/wheels/config/dump.cfc b/cli/src/commands/wheels/config/dump.cfc index 262435e7fe..2c7c3cd806 100644 --- a/cli/src/commands/wheels/config/dump.cfc +++ b/cli/src/commands/wheels/config/dump.cfc @@ -27,72 +27,77 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { string output = "", boolean noMask = false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs( - argStruct = arguments, - allowedValues = { - format: ["table", "json", "env", "cfml"] - } - ); - - // Determine environment - local.env = Len(arguments.environment) ? arguments.environment : getEnvironment(); - - // Get configuration path - local.configPath = ResolvePath("config"); - local.settingsFile = local.configPath & "/settings.cfm"; - local.envSettingsFile = local.configPath & "/" & local.env & "/settings.cfm"; - - if (!FileExists(local.settingsFile)) { - detailOutput.error("No settings.cfm file found in config directory"); - return; - } + try{ + requireWheelsApp(getCWD()); + arguments = reconstructArgs( + argStruct = arguments, + allowedValues = { + format: ["table", "json", "env", "cfml"] + } + ); - // Load configuration - local.config = loadConfiguration(local.settingsFile, local.envSettingsFile); + // Determine environment + local.env = Len(arguments.environment) ? arguments.environment : getEnvironment(); + + // Get configuration path + local.configPath = ResolvePath("config"); + local.settingsFile = local.configPath & "/settings.cfm"; + local.envSettingsFile = local.configPath & "/" & local.env & "/settings.cfm"; + + if (!FileExists(local.settingsFile)) { + detailOutput.error("No settings.cfm file found in config directory"); + return; + } - // Mask sensitive values unless --no-mask is specified - if (!arguments.noMask) { - local.config = maskSensitiveValues(local.config); - } + // Load configuration + local.config = loadConfiguration(local.settingsFile, local.envSettingsFile); - // Format output - local.outputContent = ""; - switch (arguments.format) { - case "json": - local.outputContent = SerializeJSON(local.config, false, false); - break; - case "env": - local.outputContent = formatAsEnv(local.config); - break; - case "cfml": - local.outputContent = formatAsCfml(local.config); - break; - default: - // Table format is handled differently - formatAsTable(local.config, local.env, arguments.noMask); - } + // Mask sensitive values unless --no-mask is specified + if (!arguments.noMask) { + local.config = maskSensitiveValues(local.config); + } - // Save to file if specified - if (Len(arguments.output)) { - if (arguments.format == "table") { - // For table format, we need to capture the output differently - local.tableOutput = captureTableOutput(local.config, local.env); - FileWrite(ResolvePath(arguments.output), local.tableOutput); - detailOutput.statusSuccess("Configuration saved to: #arguments.output#"); - } else { - FileWrite(ResolvePath(arguments.output), local.outputContent); - detailOutput.statusSuccess("Configuration saved to: #arguments.output#"); + // Format output + local.outputContent = ""; + switch (arguments.format) { + case "json": + local.outputContent = SerializeJSON(local.config, false, false); + break; + case "env": + local.outputContent = formatAsEnv(local.config); + break; + case "cfml": + local.outputContent = formatAsCfml(local.config); + break; + default: + // Table format is handled differently + formatAsTable(local.config, local.env, arguments.noMask); } - } else if (arguments.format != "table") { - // Output to console if not table format - if(arguments.format == "json"){ - detailOutput.line(); - detailOutput.getPrint().line(deserializeJSON(local.outputContent)).toConsole(); - } else { - detailOutput.line(); - detailOutput.output(local.outputContent); + + // Save to file if specified + if (Len(arguments.output)) { + if (arguments.format == "table") { + // For table format, we need to capture the output differently + local.tableOutput = captureTableOutput(local.config, local.env); + FileWrite(ResolvePath(arguments.output), local.tableOutput); + detailOutput.statusSuccess("Configuration saved to: #arguments.output#"); + } else { + FileWrite(ResolvePath(arguments.output), local.outputContent); + detailOutput.statusSuccess("Configuration saved to: #arguments.output#"); + } + } else if (arguments.format != "table") { + // Output to console if not table format + if(arguments.format == "json"){ + detailOutput.line(); + detailOutput.getPrint().line(deserializeJSON(local.outputContent)).toConsole(); + } else { + detailOutput.line(); + detailOutput.output(local.outputContent); + } } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } } diff --git a/cli/src/commands/wheels/config/env.cfc b/cli/src/commands/wheels/config/env.cfc index 31e56868e5..bf98048f16 100644 --- a/cli/src/commands/wheels/config/env.cfc +++ b/cli/src/commands/wheels/config/env.cfc @@ -19,36 +19,41 @@ component extends="../base" { string source="", string target="" ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs(arguments); - // Welcome message - print.line(); - print.boldMagentaLine("Wheels Environment Manager"); - print.line(); - - // Handle different actions - switch (lCase(arguments.action)) { - case "list": - listEnvironments(); - break; - case "create": - if (len(trim(arguments.target)) == 0) { - error("Target environment is required for create action"); - } - createEnvironment(arguments.target); - break; - case "copy": - if (len(trim(arguments.source)) == 0 || len(trim(arguments.target)) == 0) { - error("Source and target environments are required for copy action"); - } - copyEnvironment(arguments.source, arguments.target); - break; - default: - error("Invalid action. Choose from: list, create, copy"); - break; - } - - print.line(); + try{ + requireWheelsApp(getCWD()); + arguments = reconstructArgs(arguments); + // Welcome message + print.line(); + print.boldMagentaLine("Wheels Environment Manager"); + print.line(); + + // Handle different actions + switch (lCase(arguments.action)) { + case "list": + listEnvironments(); + break; + case "create": + if (len(trim(arguments.target)) == 0) { + error("Target environment is required for create action"); + } + createEnvironment(arguments.target); + break; + case "copy": + if (len(trim(arguments.source)) == 0 || len(trim(arguments.target)) == 0) { + error("Source and target environments are required for copy action"); + } + copyEnvironment(arguments.source, arguments.target); + break; + default: + error("Invalid action. Choose from: list, create, copy"); + break; + } + + print.line(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } /** diff --git a/cli/src/commands/wheels/config/list.cfc b/cli/src/commands/wheels/config/list.cfc index ea3e445d0e..ebdd7652a1 100644 --- a/cli/src/commands/wheels/config/list.cfc +++ b/cli/src/commands/wheels/config/list.cfc @@ -19,77 +19,82 @@ component extends="../base" { string filter="", boolean showSensitive=false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs(arguments); - // Welcome message - print.line(); - print.boldMagentaLine("Wheels Configuration Settings"); - print.line(); - - // Create URL parameters - local.urlParams = "&command=configList"; - - if (len(trim(arguments.environment))) { - local.urlParams &= "&environment=#arguments.environment#"; - } - - if (len(trim(arguments.filter))) { - local.urlParams &= "&filter=#arguments.filter#"; - } - - if (arguments.showSensitive) { - local.urlParams &= "&showSensitive=true"; - } - - // Send command to get configuration - print.line("Retrieving configuration settings..."); - local.result = $sendToCliCommand(urlstring=local.urlParams); - if(!local.result.success){ - return; - } - - // Display results - if (structKeyExists(local.result, "config") && isStruct(local.result.config)) { - // Get environment - local.env = len(trim(arguments.environment)) ? arguments.environment : local.result.environment; - print.boldYellowLine("Environment: #local.env#"); + try{ + requireWheelsApp(getCWD()); + arguments = reconstructArgs(arguments); + // Welcome message + print.line(); + print.boldMagentaLine("Wheels Configuration Settings"); print.line(); - // Build and display table - local.configTable = []; - local.keys = structKeyArray(local.result.config); - arraySort(local.keys, "textnocase"); + // Create URL parameters + local.urlParams = "&command=configList"; - for (local.key in local.keys) { - // Apply filter if specified - if (len(trim(arguments.filter)) && !findNoCase(arguments.filter, local.key)) { - continue; - } - - local.value = local.result.config[local.key]; + if (len(trim(arguments.environment))) { + local.urlParams &= "&environment=#arguments.environment#"; + } + + if (len(trim(arguments.filter))) { + local.urlParams &= "&filter=#arguments.filter#"; + } + + if (arguments.showSensitive) { + local.urlParams &= "&showSensitive=true"; + } + + // Send command to get configuration + print.line("Retrieving configuration settings..."); + local.result = $sendToCliCommand(urlstring=local.urlParams); + if(!local.result.success){ + return; + } + + // Display results + if (structKeyExists(local.result, "config") && isStruct(local.result.config)) { + // Get environment + local.env = len(trim(arguments.environment)) ? arguments.environment : local.result.environment; + print.boldYellowLine("Environment: #local.env#"); + print.line(); - // Handle sensitive information - if (!arguments.showSensitive && isSensitiveKey(local.key)) { - local.value = "********"; - } + // Build and display table + local.configTable = []; + local.keys = structKeyArray(local.result.config); + arraySort(local.keys, "textnocase"); - // Format value for display - if (isSimpleValue(local.value)) { - local.formattedValue = local.value; - } else { - local.formattedValue = serializeJSON(local.value); + for (local.key in local.keys) { + // Apply filter if specified + if (len(trim(arguments.filter)) && !findNoCase(arguments.filter, local.key)) { + continue; + } + + local.value = local.result.config[local.key]; + + // Handle sensitive information + if (!arguments.showSensitive && isSensitiveKey(local.key)) { + local.value = "********"; + } + + // Format value for display + if (isSimpleValue(local.value)) { + local.formattedValue = local.value; + } else { + local.formattedValue = serializeJSON(local.value); + } + + arrayAppend(local.configTable, [local.key, local.formattedValue]); } - arrayAppend(local.configTable, [local.key, local.formattedValue]); + // Print table + print.table(local.configTable, ["Setting", "Value"]); + } else { + print.boldRedLine("No configuration settings found"); } - // Print table - print.table(local.configTable, ["Setting", "Value"]); - } else { - print.boldRedLine("No configuration settings found"); - } - - print.line(); + print.line(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } /** diff --git a/cli/src/commands/wheels/config/set.cfc b/cli/src/commands/wheels/config/set.cfc index 3b287b684b..ca586ba224 100644 --- a/cli/src/commands/wheels/config/set.cfc +++ b/cli/src/commands/wheels/config/set.cfc @@ -18,75 +18,80 @@ component extends="../base" { string environment="development", boolean encrypt=false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs(arguments); - // Welcome message - print.line(); - print.boldMagentaLine("Wheels Configuration Manager"); - print.line(); - - // Parse the key-value pair - if (!find("=", arguments.setting)) { - error("Setting must be in the format key=value"); - } - - local.key = trim(listFirst(arguments.setting, "=")); - local.value = trim(listRest(arguments.setting, "=")); - - // Check if we need to encrypt the value - if (arguments.encrypt || isSensitiveKey(local.key)) { - if (!arguments.encrypt && isSensitiveKey(local.key)) { - print.yellowLine("Note: The setting '#local.key#' appears to contain sensitive information."); - if (confirm("Would you like to encrypt this value? [y/n]")) { - arguments.encrypt = true; + try{ + requireWheelsApp(getCWD()); + arguments = reconstructArgs(arguments); + // Welcome message + print.line(); + print.boldMagentaLine("Wheels Configuration Manager"); + print.line(); + + // Parse the key-value pair + if (!find("=", arguments.setting)) { + error("Setting must be in the format key=value"); + } + + local.key = trim(listFirst(arguments.setting, "=")); + local.value = trim(listRest(arguments.setting, "=")); + + // Check if we need to encrypt the value + if (arguments.encrypt || isSensitiveKey(local.key)) { + if (!arguments.encrypt && isSensitiveKey(local.key)) { + print.yellowLine("Note: The setting '#local.key#' appears to contain sensitive information."); + if (confirm("Would you like to encrypt this value? [y/n]")) { + arguments.encrypt = true; + } + } + + if (arguments.encrypt) { + print.yellowLine("Encrypting value for '#local.key#'..."); + // Note: The actual encryption would happen in the controller + local.value = "encrypted:" & local.value; } } - + + // Create URL parameters + local.urlParams = "&command=configSet&key=#urlEncodedFormat(local.key)#&value=#urlEncodedFormat(local.value)#&environment=#arguments.environment#"; + if (arguments.encrypt) { - print.yellowLine("Encrypting value for '#local.key#'..."); - // Note: The actual encryption would happen in the controller - local.value = "encrypted:" & local.value; - } - } - - // Create URL parameters - local.urlParams = "&command=configSet&key=#urlEncodedFormat(local.key)#&value=#urlEncodedFormat(local.value)#&environment=#arguments.environment#"; - - if (arguments.encrypt) { - local.urlParams &= "&encrypt=true"; - } - - // Send command to set configuration - print.line("Setting configuration value..."); - local.result = $sendToCliCommand(urlstring=local.urlParams); - if(!local.result.success){ - return; - } - - // Display results - if (structKeyExists(local.result, "success") && local.result.success) { - print.boldGreenLine("Configuration value set successfully"); - print.yellowLine("Environment: #arguments.environment#"); - print.yellowLine("Key: #local.key#"); - - // Don't show the actual value for sensitive information - if (isSensitiveKey(local.key) && !arguments.encrypt) { - print.yellowLine("Value: ********"); - } else { - print.yellowLine("Value: #local.value#"); + local.urlParams &= "&encrypt=true"; } - - if (arguments.encrypt) { - print.yellowLine("Encryption: Enabled"); + + // Send command to set configuration + print.line("Setting configuration value..."); + local.result = $sendToCliCommand(urlstring=local.urlParams); + if(!local.result.success){ + return; } - } else { - print.boldRedLine("Failed to set configuration value"); - if (structKeyExists(local.result, "message")) { - print.redLine(local.result.message); + + // Display results + if (structKeyExists(local.result, "success") && local.result.success) { + print.boldGreenLine("Configuration value set successfully"); + print.yellowLine("Environment: #arguments.environment#"); + print.yellowLine("Key: #local.key#"); + + // Don't show the actual value for sensitive information + if (isSensitiveKey(local.key) && !arguments.encrypt) { + print.yellowLine("Value: ********"); + } else { + print.yellowLine("Value: #local.value#"); + } + + if (arguments.encrypt) { + print.yellowLine("Encryption: Enabled"); + } + } else { + print.boldRedLine("Failed to set configuration value"); + if (structKeyExists(local.result, "message")) { + print.redLine(local.result.message); + } } - } - - print.line(); + + print.line(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } /** diff --git a/cli/src/commands/wheels/dbmigrate/create/blank.cfc b/cli/src/commands/wheels/dbmigrate/create/blank.cfc index 7575283a2d..4313b6fce9 100644 --- a/cli/src/commands/wheels/dbmigrate/create/blank.cfc +++ b/cli/src/commands/wheels/dbmigrate/create/blank.cfc @@ -25,32 +25,37 @@ component aliases='wheels db create blank' extends="../../base" { required string name, string description = "" ) { - // Reconstruct arguments for handling --prefixed options - arguments = reconstructArgs(arguments); - - // Output detail header - detailOutput.header("Migration Generation"); + try{ + // Reconstruct arguments for handling --prefixed options + arguments = reconstructArgs(arguments); + + // Output detail header + detailOutput.header("Migration Generation"); - // Get Template - var content = fileRead(getTemplate("dbmigrate/blank.txt")); + // Get Template + var content = fileRead(getTemplate("dbmigrate/blank.txt")); - // Replace template variables - if (len(trim(arguments.description))) { - content = replaceNoCase(content, "|DBMigrateDescription|", arguments.description, "all"); - } + // Replace template variables + if (len(trim(arguments.description))) { + content = replaceNoCase(content, "|DBMigrateDescription|", arguments.description, "all"); + } - // Make File - var migrationPath = $createMigrationFile(name=lcase(trim(arguments.name)), action="blank", content=content); - - detailOutput.create(migrationPath); - detailOutput.line(); - detailOutput.statusSuccess("Blank migration created successfully!"); - - var nextSteps = []; - arrayAppend(nextSteps, "1. Edit the migration file: #migrationPath#"); - arrayAppend(nextSteps, "2. Start your server: server start"); - arrayAppend(nextSteps, "3. Check migration status: wheels dbmigrate info"); - arrayAppend(nextSteps, "4. Run the migration: wheels dbmigrate latest"); - detailOutput.nextSteps(nextSteps); + // Make File + var migrationPath = $createMigrationFile(name=lcase(trim(arguments.name)), action="blank", content=content); + + detailOutput.create(migrationPath); + detailOutput.line(); + detailOutput.statusSuccess("Blank migration created successfully!"); + + var nextSteps = []; + arrayAppend(nextSteps, "1. Edit the migration file: #migrationPath#"); + arrayAppend(nextSteps, "2. Start your server: server start"); + arrayAppend(nextSteps, "3. Check migration status: wheels dbmigrate info"); + arrayAppend(nextSteps, "4. Run the migration: wheels dbmigrate latest"); + detailOutput.nextSteps(nextSteps); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } } \ No newline at end of file diff --git a/cli/src/commands/wheels/dbmigrate/create/column.cfc b/cli/src/commands/wheels/dbmigrate/create/column.cfc index b3b3969961..8493c7c6bb 100644 --- a/cli/src/commands/wheels/dbmigrate/create/column.cfc +++ b/cli/src/commands/wheels/dbmigrate/create/column.cfc @@ -44,74 +44,79 @@ boolean allowNull=true, number limit, number precision, - number scale) { - - // Reconstruct arguments for handling -- prefixed options - arguments = reconstructArgs( - argStruct = arguments, - allowedValues = { - dataType: ["biginteger", "binary", "boolean", "date", "datetime", "decimal", "float", "integer", "string", "text", "time", "timestamp", "uuid"] - } - ); - - // Get Template - var content=fileRead(getTemplate("dbmigrate/create-column.txt")); - var argumentArr=[]; - var argumentString=""; + number scale + ) { + try{ + // Reconstruct arguments for handling -- prefixed options + arguments = reconstructArgs( + argStruct = arguments, + allowedValues = { + dataType: ["biginteger", "binary", "boolean", "date", "datetime", "decimal", "float", "integer", "string", "text", "time", "timestamp", "uuid"] + } + ); + + // Get Template + var content=fileRead(getTemplate("dbmigrate/create-column.txt")); + var argumentArr=[]; + var argumentString=""; - // Changes here - content=replaceNoCase(content, "|tableName|", "#arguments.tableName#", "all"); - content=replaceNoCase(content, "|columnType|", "#arguments.dataType#", "all"); - content=replaceNoCase(content, "|columnName|", "#arguments.name#", "all"); - //content=replaceNoCase(content, "|referenceName|", "#referenceName#", "all"); + // Changes here + content=replaceNoCase(content, "|tableName|", "#arguments.tableName#", "all"); + content=replaceNoCase(content, "|columnType|", "#arguments.dataType#", "all"); + content=replaceNoCase(content, "|columnName|", "#arguments.name#", "all"); + //content=replaceNoCase(content, "|referenceName|", "#referenceName#", "all"); - // Construct additional arguments(only add/replace if passed through) - if(structKeyExists(arguments,"default") && len(arguments.default)){ - if(isnumeric(arguments.default)){ - arrayAppend(argumentArr, "default = #arguments.default#"); - } else { - arrayAppend(argumentArr, "default = '#arguments.default#'"); + // Construct additional arguments(only add/replace if passed through) + if(structKeyExists(arguments,"default") && len(arguments.default)){ + if(isnumeric(arguments.default)){ + arrayAppend(argumentArr, "default = #arguments.default#"); + } else { + arrayAppend(argumentArr, "default = '#arguments.default#'"); + } + } + if(structKeyExists(arguments,"allowNull") && len(arguments.allowNull) && isBoolean(arguments.allowNull)){ + arrayAppend(argumentArr, "allowNull = #arguments.allowNull#"); + } + if(structKeyExists(arguments,"limit") && len(arguments.limit) && isnumeric(arguments.limit) && arguments.limit != 0){ + arrayAppend(argumentArr, "limit = #arguments.limit#"); + } + if(structKeyExists(arguments,"precision") && len(arguments.precision) && isnumeric(arguments.precision) && arguments.precision != 0){ + arrayAppend(argumentArr, "precision = #arguments.precision#"); + } + if(structKeyExists(arguments,"scale") && len(arguments.scale) && isnumeric(arguments.scale) && arguments.scale != 0){ + arrayAppend(argumentArr, "scale = #arguments.scale#"); + } + if(arrayLen(argumentArr)){ + argumentString&=", "; + argumentString&=$constructArguments(argumentArr); } - } - if(structKeyExists(arguments,"allowNull") && len(arguments.allowNull) && isBoolean(arguments.allowNull)){ - arrayAppend(argumentArr, "allowNull = #arguments.allowNull#"); - } - if(structKeyExists(arguments,"limit") && len(arguments.limit) && isnumeric(arguments.limit) && arguments.limit != 0){ - arrayAppend(argumentArr, "limit = #arguments.limit#"); - } - if(structKeyExists(arguments,"precision") && len(arguments.precision) && isnumeric(arguments.precision) && arguments.precision != 0){ - arrayAppend(argumentArr, "precision = #arguments.precision#"); - } - if(structKeyExists(arguments,"scale") && len(arguments.scale) && isnumeric(arguments.scale) && arguments.scale != 0){ - arrayAppend(argumentArr, "scale = #arguments.scale#"); - } - if(arrayLen(argumentArr)){ - argumentString&=", "; - argumentString&=$constructArguments(argumentArr); - } - // Finally, replace |arguments| with appropriate string - content=replaceNoCase(content, "|arguments|", "#argumentString#", "all"); - //content=replaceNoCase(content, "|null|", "#null#", "all"); - //content=replaceNoCase(content, "|limit|", "#limit#", "all"); - //content=replaceNoCase(content, "|precision|", "#precision#", "all"); - //content=replaceNoCase(content, "|scale|", "#scale#", "all"); + // Finally, replace |arguments| with appropriate string + content=replaceNoCase(content, "|arguments|", "#argumentString#", "all"); + //content=replaceNoCase(content, "|null|", "#null#", "all"); + //content=replaceNoCase(content, "|limit|", "#limit#", "all"); + //content=replaceNoCase(content, "|precision|", "#precision#", "all"); + //content=replaceNoCase(content, "|scale|", "#scale#", "all"); - // Output detail header - detailOutput.header("Migration Generation"); - - // Make File - var migrationPath = $createMigrationFile(name=lcase(trim(arguments.tableName)) & '_' & lcase(trim(arguments.name)), action="create_column", content=content); - - detailOutput.create(migrationPath); - detailOutput.line(); - detailOutput.statusSuccess("Column migration created successfully!"); - - var nextSteps = []; - arrayAppend(nextSteps, "Review the migration file: #migrationPath#"); - arrayAppend(nextSteps, "Run the migration: wheels dbmigrate up"); - arrayAppend(nextSteps, "Or run all pending migrations: wheels dbmigrate latest"); - detailOutput.nextSteps(nextSteps); + // Output detail header + detailOutput.header("Migration Generation"); + + // Make File + var migrationPath = $createMigrationFile(name=lcase(trim(arguments.tableName)) & '_' & lcase(trim(arguments.name)), action="create_column", content=content); + + detailOutput.create(migrationPath); + detailOutput.line(); + detailOutput.statusSuccess("Column migration created successfully!"); + + var nextSteps = []; + arrayAppend(nextSteps, "Review the migration file: #migrationPath#"); + arrayAppend(nextSteps, "Run the migration: wheels dbmigrate up"); + arrayAppend(nextSteps, "Or run all pending migrations: wheels dbmigrate latest"); + detailOutput.nextSteps(nextSteps); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } function $constructArguments(args, string operator=","){ diff --git a/cli/src/commands/wheels/dbmigrate/create/table.cfc b/cli/src/commands/wheels/dbmigrate/create/table.cfc index 8e46ef95b2..dc5b059905 100644 --- a/cli/src/commands/wheels/dbmigrate/create/table.cfc +++ b/cli/src/commands/wheels/dbmigrate/create/table.cfc @@ -35,35 +35,41 @@ required string name, boolean force = false, boolean id = true, - string primaryKey="id") { - - // Reconstruct arguments for handling --prefixed options - arguments = reconstructArgs(arguments); - + string primaryKey="id" + ) { + try{ + // Reconstruct arguments for handling --prefixed options + arguments = reconstructArgs(arguments); + - // Get Template - var content=fileRead(getTemplate("dbmigrate/create-table.txt")); + // Get Template + var content=fileRead(getTemplate("dbmigrate/create-table.txt")); - // Changes here - content=replaceNoCase(content, "|tableName|", "#name#", "all"); - content=replaceNoCase(content, "|force|", "#force#", "all"); - content=replaceNoCase(content, "|id|", "#id#", "all"); - content=replaceNoCase(content, "|primaryKey|", "#arguments.primaryKey#", "all"); + // Changes here + content=replaceNoCase(content, "|tableName|", "#name#", "all"); + content=replaceNoCase(content, "|force|", "#force#", "all"); + content=replaceNoCase(content, "|id|", "#id#", "all"); + content=replaceNoCase(content, "|primaryKey|", "#arguments.primaryKey#", "all"); - // Output detail header - detailOutput.header("Migration Generation"); - - // Make File - var migrationPath = $createMigrationFile(name=lcase(trim(arguments.name)), action="create_table", content=content); - - detailOutput.create(migrationPath); - detailOutput.line(); - detailOutput.statusSuccess("Table migration created successfully!"); + // Output detail header + detailOutput.header("Migration Generation"); + + // Make File + var migrationPath = $createMigrationFile(name=lcase(trim(arguments.name)), action="create_table", content=content); + + detailOutput.create(migrationPath); + detailOutput.line(); + detailOutput.statusSuccess("Table migration created successfully!"); + + var nextSteps = []; + arrayAppend(nextSteps, "Edit the migration to add columns: #migrationPath#"); + arrayAppend(nextSteps, "Run the migration: wheels dbmigrate up"); + arrayAppend(nextSteps, "Generate a model for this table: wheels generate model #arguments.name#"); + detailOutput.nextSteps(nextSteps); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } - var nextSteps = []; - arrayAppend(nextSteps, "Edit the migration to add columns: #migrationPath#"); - arrayAppend(nextSteps, "Run the migration: wheels dbmigrate up"); - arrayAppend(nextSteps, "Generate a model for this table: wheels generate model #arguments.name#"); - detailOutput.nextSteps(nextSteps); } } \ No newline at end of file diff --git a/cli/src/commands/wheels/dbmigrate/down.cfc b/cli/src/commands/wheels/dbmigrate/down.cfc index 0f13eae97e..e59cecc6cc 100644 --- a/cli/src/commands/wheels/dbmigrate/down.cfc +++ b/cli/src/commands/wheels/dbmigrate/down.cfc @@ -9,65 +9,71 @@ component aliases='wheels db down' extends="../base" { * **/ function run( ) { - var DBMigrateInfo=$sendToCliCommand(); - if(!DBMigrateInfo.success){ - return; - } - var migrations=DBMigrateInfo.migrations; + try{ + var DBMigrateInfo=$sendToCliCommand(); + if(!DBMigrateInfo.success){ + return; + } + var migrations=DBMigrateInfo.migrations; - //print.line(Formatter.formatJson( $getDBMigrateInfo() ) ); + //print.line(Formatter.formatJson( $getDBMigrateInfo() ) ); - // Check we're not at 0 - if(DBMigrateInfo.currentVersion == 0){ - detailOutput.statusWarning("No migrations have been run yet. Database is at version 0."); - detailOutput.statusInfo("Use 'wheels dbmigrate latest' to run migrations."); - return; - } + // Check we're not at 0 + if(DBMigrateInfo.currentVersion == 0){ + detailOutput.statusWarning("No migrations have been run yet. Database is at version 0."); + detailOutput.statusInfo("Use 'wheels dbmigrate latest' to run migrations."); + return; + } - // Check if migrations array is empty (files deleted after running migrations) - if(!arrayLen(migrations)){ - detailOutput.statusFailed("No migration files found, but database is at version #DBMigrateInfo.currentVersion#."); - detailOutput.statusWarning("Migration files may have been deleted after running migrations."); - detailOutput.nextSteps([ - "Restore the migration files from source control", - "Reset the schema version table manually", - "Run 'wheels dbmigrate info' to see current status" - ]); - return; - } + // Check if migrations array is empty (files deleted after running migrations) + if(!arrayLen(migrations)){ + detailOutput.statusFailed("No migration files found, but database is at version #DBMigrateInfo.currentVersion#."); + detailOutput.statusWarning("Migration files may have been deleted after running migrations."); + detailOutput.nextSteps([ + "Restore the migration files from source control", + "Reset the schema version table manually", + "Run 'wheels dbmigrate info' to see current status" + ]); + return; + } - // Get current version as an index of the migration array - var currentIndex = 0; - var newIndex = 0; - var migrateTo = 0; - migrations.each(function(migration,i,array){ - if(migration.version == DBMigrateInfo.currentVersion){ - currentIndex = i; - } - }); + // Get current version as an index of the migration array + var currentIndex = 0; + var newIndex = 0; + var migrateTo = 0; + migrations.each(function(migration,i,array){ + if(migration.version == DBMigrateInfo.currentVersion){ + currentIndex = i; + } + }); - // Check if current version was found in migrations array - if(currentIndex == 0){ - detailOutput.statusFailed("Current database version (#DBMigrateInfo.currentVersion#) not found in migrations."); - detailOutput.statusWarning("This may indicate a migration file was deleted or the schema version table is corrupt."); - detailOutput.metric("Current version in database", DBMigrateInfo.currentVersion); - detailOutput.metric("Available migrations", arrayLen(migrations)); - return; - } + // Check if current version was found in migrations array + if(currentIndex == 0){ + detailOutput.statusFailed("Current database version (#DBMigrateInfo.currentVersion#) not found in migrations."); + detailOutput.statusWarning("This may indicate a migration file was deleted or the schema version table is corrupt."); + detailOutput.metric("Current version in database", DBMigrateInfo.currentVersion); + detailOutput.metric("Available migrations", arrayLen(migrations)); + return; + } - newIndex = --currentIndex; - if(newIndex > 0){ - migrateTo=migrations[newIndex]["version"]; + newIndex = --currentIndex; + if(newIndex > 0){ + migrateTo=migrations[newIndex]["version"]; + } + + detailOutput.statusInfo("Migrating to version #migrateTo#"); + command('wheels dbmigrate exec') + .params(version=migrateTo) + .run(); + if(migrateTo == 0){ + detailOutput.statusSuccess("Database should now be empty."); + } + command('wheels dbmigrate info').run(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } - detailOutput.statusInfo("Migrating to version #migrateTo#"); - command('wheels dbmigrate exec') - .params(version=migrateTo) - .run(); - if(migrateTo == 0){ - detailOutput.statusSuccess("Database should now be empty."); - } - command('wheels dbmigrate info').run(); } } diff --git a/cli/src/commands/wheels/dbmigrate/exec.cfc b/cli/src/commands/wheels/dbmigrate/exec.cfc index 2194ee88f4..0e2c4eb384 100644 --- a/cli/src/commands/wheels/dbmigrate/exec.cfc +++ b/cli/src/commands/wheels/dbmigrate/exec.cfc @@ -12,26 +12,33 @@ component aliases='wheels db exec' extends="../base" { * Migrate to specific version * @version.hint Version to migrate to **/ - function run( required string version ) { - // Reconstruct arguments for handling --prefixed options - arguments = reconstructArgs(arguments); + function run( + required string version + ) { + try{ + // Reconstruct arguments for handling --prefixed options + arguments = reconstructArgs(arguments); - var loc={ - version = arguments.version - } + var loc={ + version = arguments.version + } - detailOutput.header("Migration Execution", 50); - detailOutput.metric("Target Version", loc.version); - detailOutput.divider(); - var result = $sendToCliCommand("&command=migrateTo&version=#loc.version#"); - if(!local.result.success){ - return; - } - - if (structKeyExists(result, "success") && result.success) { - detailOutput.statusSuccess("Migration completed successfully!"); - } else { - detailOutput.statusFailed("Migration failed!"); + detailOutput.header("Migration Execution", 50); + detailOutput.metric("Target Version", loc.version); + detailOutput.divider(); + var result = $sendToCliCommand("&command=migrateTo&version=#loc.version#"); + if(!local.result.success){ + return; + } + + if (structKeyExists(result, "success") && result.success) { + detailOutput.statusSuccess("Migration completed successfully!"); + } else { + detailOutput.statusFailed("Migration failed!"); + } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } } } \ No newline at end of file diff --git a/cli/src/commands/wheels/dbmigrate/info.cfc b/cli/src/commands/wheels/dbmigrate/info.cfc index abfe1786c0..7373d23536 100644 --- a/cli/src/commands/wheels/dbmigrate/info.cfc +++ b/cli/src/commands/wheels/dbmigrate/info.cfc @@ -9,54 +9,59 @@ component aliases='wheels db info' extends="../base" { * Display DB Migrate info **/ function run( ) { - local.results = $sendToCliCommand(); - if(!local.results.success){ - return; - } - local.migrations = local.results.migrations.reverse(); - // calculate the available migrations by stepping through the migration array - local.available = 0; - for (local.migration in local.migrations) { - if (local.migration.status == "") { - local.available++; - } - } - - detailOutput.header("Database Migration Status", 50); - - detailOutput.subHeader("Database Information", 50); - detailOutput.metric("Datasource", local.results.datasource); - detailOutput.metric("Database Type", local.results.databaseType); - - detailOutput.subHeader("Migration Status", 50); - detailOutput.metric("Total Migrations", arrayLen(local.results.migrations)); - detailOutput.metric("Available Migrations", local.available); - detailOutput.metric("Current Version", local.results.currentVersion); - detailOutput.metric("Latest Version", local.results.lastVersion); - - detailOutput.divider(); - - if (arrayLen(local.migrations)) { - detailOutput.subHeader("Migration Files", 50); - - var migrationData = []; + try{ + local.results = $sendToCliCommand(); + if(!local.results.success){ + return; + } + local.migrations = local.results.migrations.reverse(); + // calculate the available migrations by stepping through the migration array + local.available = 0; for (local.migration in local.migrations) { - arrayAppend(migrationData, { - status: local.migration.status == "" ? "" : local.migration.status, - file: local.migration.CFCFILE - }); + if (local.migration.status == "") { + local.available++; + } } + + detailOutput.header("Database Migration Status", 50); - detailOutput.getPrint().table( - data = migrationData, - headers = ["Status", "Migration File"] - ).toConsole(); + detailOutput.subHeader("Database Information", 50); + detailOutput.metric("Datasource", local.results.datasource); + detailOutput.metric("Database Type", local.results.databaseType); - detailOutput.line(); - } - - if (local.results.message != "Returning what I know..") { - detailOutput.statusInfo(local.results.message); + detailOutput.subHeader("Migration Status", 50); + detailOutput.metric("Total Migrations", arrayLen(local.results.migrations)); + detailOutput.metric("Available Migrations", local.available); + detailOutput.metric("Current Version", local.results.currentVersion); + detailOutput.metric("Latest Version", local.results.lastVersion); + + detailOutput.divider(); + + if (arrayLen(local.migrations)) { + detailOutput.subHeader("Migration Files", 50); + + var migrationData = []; + for (local.migration in local.migrations) { + arrayAppend(migrationData, { + status: local.migration.status == "" ? "" : local.migration.status, + file: local.migration.CFCFILE + }); + } + + detailOutput.getPrint().table( + data = migrationData, + headers = ["Status", "Migration File"] + ).toConsole(); + + detailOutput.line(); + } + + if (local.results.message != "Returning what I know..") { + detailOutput.statusInfo(local.results.message); + } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } } } \ No newline at end of file diff --git a/cli/src/commands/wheels/dbmigrate/latest.cfc b/cli/src/commands/wheels/dbmigrate/latest.cfc index 23dbbb5fed..5814effbff 100644 --- a/cli/src/commands/wheels/dbmigrate/latest.cfc +++ b/cli/src/commands/wheels/dbmigrate/latest.cfc @@ -9,49 +9,56 @@ component aliases='wheels db latest,wheels db migrate' extends="../base" { * @version Optional version to migrate to (0 to initialize, or specific version number) * @help Migrate database to latest version or specified version **/ - function run(string version = "") { - // Reconstruct arguments for handling --prefixed options - arguments = reconstructArgs(arguments); + function run( + string version = "" + ) { + try{ + // Reconstruct arguments for handling --prefixed options + arguments = reconstructArgs(arguments); - // Support for wheels db migrate version=0 syntax - if (Len(arguments.version)) { - if (arguments.version == "0") { - print.line("Initializing migration tables...").toConsole(); - // Run reset to version 0 which initializes the migration table - command('wheels dbmigrate reset').run(); - detailOutput.statusSuccess("Migration table initialized successfully"); - } else { - // Migrate to specific version - print.line("Migrating database to version #arguments.version#...").toConsole(); - command('wheels dbmigrate exec').params(version=arguments.version).run(); - } - } else { - // Default behavior - migrate to latest - try { - var DBMigrateInfo = $sendToCliCommand("&command=info"); - if(!local.DBMigrateInfo.success){ - return; + // Support for wheels db migrate version=0 syntax + if (Len(arguments.version)) { + if (arguments.version == "0") { + print.line("Initializing migration tables...").toConsole(); + // Run reset to version 0 which initializes the migration table + command('wheels dbmigrate reset').run(); + detailOutput.statusSuccess("Migration table initialized successfully"); + } else { + // Migrate to specific version + print.line("Migrating database to version #arguments.version#...").toConsole(); + command('wheels dbmigrate exec').params(version=arguments.version).run(); } - - // Check if we got a valid response - if (!DBMigrateInfo.success || !structKeyExists(DBMigrateInfo, "lastVersion")) { - detailOutput.error("Unable to retrieve migration information from the application. Please ensure your server is running and the application is properly configured."); + } else { + // Default behavior - migrate to latest + try { + var DBMigrateInfo = $sendToCliCommand("&command=info"); + if(!local.DBMigrateInfo.success){ + return; + } + + // Check if we got a valid response + if (!DBMigrateInfo.success || !structKeyExists(DBMigrateInfo, "lastVersion")) { + detailOutput.error("Unable to retrieve migration information from the application. Please ensure your server is running and the application is properly configured."); + return; + } + + detailOutput.header("Updating Database Schema to Latest Version"); + detailOutput.metric("Latest Version", DBMigrateInfo.lastVersion); + + command('wheels dbmigrate exec').params(version=DBMigrateInfo.lastVersion).run(); + } catch (any e) { + detailOutput.error("Failed to get migration information: #e.message#"); return; } - - detailOutput.header("Updating Database Schema to Latest Version"); - detailOutput.metric("Latest Version", DBMigrateInfo.lastVersion); - - command('wheels dbmigrate exec').params(version=DBMigrateInfo.lastVersion).run(); - } catch (any e) { - detailOutput.error("Failed to get migration information: #e.message#"); - return; } + + // Add a separator before the info command output + detailOutput.line(); + command('wheels dbmigrate info').run(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } - - // Add a separator before the info command output - detailOutput.line(); - command('wheels dbmigrate info').run(); } } \ No newline at end of file diff --git a/cli/src/commands/wheels/dbmigrate/remove/table.cfc b/cli/src/commands/wheels/dbmigrate/remove/table.cfc index c86cb39a19..af80432ae9 100644 --- a/cli/src/commands/wheels/dbmigrate/remove/table.cfc +++ b/cli/src/commands/wheels/dbmigrate/remove/table.cfc @@ -14,28 +14,34 @@ component aliases='wheels db remove table' extends="../../base" { * **/ function run( - required string name ) { - arguments = reconstructArgs(arguments); - // Get Template - var content=fileRead(getTemplate("dbmigrate/remove-table.txt")); + required string name + ) { + try{ + arguments = reconstructArgs(arguments); + // Get Template + var content=fileRead(getTemplate("dbmigrate/remove-table.txt")); - // Changes here - content=replaceNoCase(content, "|tableName|", "#name#", "all"); + // Changes here + content=replaceNoCase(content, "|tableName|", "#name#", "all"); - // Output detail header - detailOutput.header("Migration Generation"); - - // Make File - var migrationPath = $createMigrationFile(name=lcase(trim(arguments.name)), action="remove_table", content=content); - - detailOutput.remove(migrationPath); - detailOutput.line(); - detailOutput.statusSuccess("Table removal migration created successfully!"); - - var nextSteps = []; - arrayAppend(nextSteps, "Review the migration file: #migrationPath#"); - arrayAppend(nextSteps, "Run the migration: wheels dbmigrate up"); - arrayAppend(nextSteps, "Run all pending migrations: wheels dbmigrate latest"); - detailOutput.nextSteps(nextSteps); + // Output detail header + detailOutput.header("Migration Generation"); + + // Make File + var migrationPath = $createMigrationFile(name=lcase(trim(arguments.name)), action="remove_table", content=content); + + detailOutput.remove(migrationPath); + detailOutput.line(); + detailOutput.statusSuccess("Table removal migration created successfully!"); + + var nextSteps = []; + arrayAppend(nextSteps, "Review the migration file: #migrationPath#"); + arrayAppend(nextSteps, "Run the migration: wheels dbmigrate up"); + arrayAppend(nextSteps, "Run all pending migrations: wheels dbmigrate latest"); + detailOutput.nextSteps(nextSteps); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } } \ No newline at end of file diff --git a/cli/src/commands/wheels/dbmigrate/reset.cfc b/cli/src/commands/wheels/dbmigrate/reset.cfc index fb8283c67e..9f3cdd14cf 100644 --- a/cli/src/commands/wheels/dbmigrate/reset.cfc +++ b/cli/src/commands/wheels/dbmigrate/reset.cfc @@ -7,13 +7,18 @@ component aliases='wheels db reset' extends="../base" { * **/ function run() { - var DBMigrateInfo=$sendToCliCommand(); - if(!DBMigrateInfo.success){ - return; + try{ + var DBMigrateInfo=$sendToCliCommand(); + if(!DBMigrateInfo.success){ + return; + } + print.line("Resetting Database Schema").toConsole(); + command('wheels dbmigrate exec').params(version=0).run(); + command('wheels dbmigrate info').run(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } - print.line("Resetting Database Schema").toConsole(); - command('wheels dbmigrate exec').params(version=0).run(); - command('wheels dbmigrate info').run(); } } \ No newline at end of file diff --git a/cli/src/commands/wheels/dbmigrate/up.cfc b/cli/src/commands/wheels/dbmigrate/up.cfc index 89e0814799..5501732656 100644 --- a/cli/src/commands/wheels/dbmigrate/up.cfc +++ b/cli/src/commands/wheels/dbmigrate/up.cfc @@ -9,40 +9,45 @@ component aliases='wheels db up' extends="../base" { * **/ function run() { - var DBMigrateInfo = $sendToCliCommand(); - if(!DBMigrateInfo.success){ - return; - } - var migrations = DBMigrateInfo.migrations; - - // Check we're not already at the latest version - if (DBMigrateInfo.currentVersion == DBMigrateInfo.lastVersion) { - detailOutput.statusSuccess("We're all up to date already!"); - return; - } + try{ + var DBMigrateInfo = $sendToCliCommand(); + if(!DBMigrateInfo.success){ + return; + } + var migrations = DBMigrateInfo.migrations; - // Get current version as an index of the migration array - var currentIndex = 0; - var newIndex = 0; - migrations.each(function(migration, i, array) { - if (migration.version == DBMigrateInfo.currentVersion) { - currentIndex = i; + // Check we're not already at the latest version + if (DBMigrateInfo.currentVersion == DBMigrateInfo.lastVersion) { + detailOutput.statusSuccess("We're all up to date already!"); + return; } - }); - if (currentIndex < arrayLen(migrations)) { - newIndex = ++currentIndex; - detailOutput.statusInfo("Migrating to #migrations[newIndex]['cfcfile']#"); - detailOutput.migrate(migrations[newIndex]['cfcfile']); - - command('wheels dbmigrate exec') - .params(version = migrations[newIndex]["version"]) - .run(); - } else { - detailOutput.statusWarning("No more versions to go to?"); + // Get current version as an index of the migration array + var currentIndex = 0; + var newIndex = 0; + migrations.each(function(migration, i, array) { + if (migration.version == DBMigrateInfo.currentVersion) { + currentIndex = i; + } + }); + + if (currentIndex < arrayLen(migrations)) { + newIndex = ++currentIndex; + detailOutput.statusInfo("Migrating to #migrations[newIndex]['cfcfile']#"); + detailOutput.migrate(migrations[newIndex]['cfcfile']); + + command('wheels dbmigrate exec') + .params(version = migrations[newIndex]["version"]) + .run(); + } else { + detailOutput.statusWarning("No more versions to go to?"); + } + detailOutput.line(); + command('wheels dbmigrate info').run(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } - detailOutput.line(); - command('wheels dbmigrate info').run(); } } \ No newline at end of file diff --git a/cli/src/commands/wheels/deps.cfc b/cli/src/commands/wheels/deps.cfc index ee19d1dbbf..8887c9aeba 100644 --- a/cli/src/commands/wheels/deps.cfc +++ b/cli/src/commands/wheels/deps.cfc @@ -25,49 +25,55 @@ component extends="base" { string version="", boolean dev=false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs( - argStruct=arguments, - allowedValues={ - action=["list", "install", "update", "remove", "report"] + try { + requireWheelsApp(getCWD()); + arguments = reconstructArgs( + argStruct=arguments, + allowedValues={ + action=["list", "install", "update", "remove", "report"] + } + ); + + // Welcome message + detailOutput.header("Wheels Dependency Manager"); + + // Handle different actions + switch (lCase(arguments.action)) { + case "list": + listDependencies(); + break; + case "install": + if (len(trim(arguments.name)) == 0) { + detailOutput.error("Name parameter is required for install action"); + return; + } + installDependency(arguments.name, arguments.version, arguments.dev); + break; + case "update": + if (len(trim(arguments.name)) == 0) { + detailOutput.error("Name parameter is required for update action"); + return; + } + updateDependency(arguments.name); + break; + case "remove": + if (len(trim(arguments.name)) == 0) { + detailOutput.error("Name parameter is required for remove action"); + return; + } + removeDependency(arguments.name); + break; + case "report": + generateDependencyReport(); + break; } - ); + + detailOutput.line(); - // Welcome message - detailOutput.header("Wheels Dependency Manager"); - - // Handle different actions - switch (lCase(arguments.action)) { - case "list": - listDependencies(); - break; - case "install": - if (len(trim(arguments.name)) == 0) { - detailOutput.error("Name parameter is required for install action"); - return; - } - installDependency(arguments.name, arguments.version, arguments.dev); - break; - case "update": - if (len(trim(arguments.name)) == 0) { - detailOutput.error("Name parameter is required for update action"); - return; - } - updateDependency(arguments.name); - break; - case "remove": - if (len(trim(arguments.name)) == 0) { - detailOutput.error("Name parameter is required for remove action"); - return; - } - removeDependency(arguments.name); - break; - case "report": - generateDependencyReport(); - break; - } - - detailOutput.line(); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } /** diff --git a/cli/src/commands/wheels/destroy.cfc b/cli/src/commands/wheels/destroy.cfc index e6b868b556..5c2a6a6860 100644 --- a/cli/src/commands/wheels/destroy.cfc +++ b/cli/src/commands/wheels/destroy.cfc @@ -17,7 +17,8 @@ component aliases='wheels d' extends="base" { * @type.hint Type of component to destroy (resource, controller, model, view). Default is resource * @name.hint Name of object to destroy **/ - function run(required string name, string type="resource") { + function run( + required string name, string type="resource") { requireWheelsApp(getCWD()); arguments=reconstructArgs(arguments); diff --git a/cli/src/commands/wheels/docs/generate.cfc b/cli/src/commands/wheels/docs/generate.cfc index a12b93bc47..f51547ca12 100644 --- a/cli/src/commands/wheels/docs/generate.cfc +++ b/cli/src/commands/wheels/docs/generate.cfc @@ -29,116 +29,121 @@ component extends="../base" { boolean serve = false, boolean verbose = false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs( - argStruct=arguments, - allowedValues={ - format: ["html", "json", "markdown"], - template: ["default", "minimal", "detailed"], - include: ["models", "controllers", "views", "services"] - }, - allowCommaSeparated=["include"] - ); - - detailOutput.header("Documentation Generator"); - print.line("Generating documentation...").toConsole(); - detailOutput.line(); - - var outputPath = resolvePath(arguments.output); - var componentsToDocument = listToArray(arguments.include); - - // Ensure output directory exists - if (!directoryExists(outputPath)) { - directoryCreate(outputPath, true); - detailOutput.create("directory #arguments.output#"); - } - - var documentedComponents = { - models = [], - controllers = [], - views = [], - services = [], - total = 0 - }; - - detailOutput.subHeader("Scanning Source Files"); - - // Document each component type - for (var componentType in componentsToDocument) { - if (arguments.verbose) { - print.line("Documenting #componentType#...").toConsole(); - } - - var documented = documentComponents( - type = componentType, - outputPath = outputPath, - format = arguments.format, - template = arguments.template, - verbose = arguments.verbose + try { + requireWheelsApp(getCWD()); + arguments = reconstructArgs( + argStruct=arguments, + allowedValues={ + format: ["html", "json", "markdown"], + template: ["default", "minimal", "detailed"], + include: ["models", "controllers", "views", "services"] + }, + allowCommaSeparated=["include"] ); - documentedComponents[componentType] = documented; - documentedComponents.total += arrayLen(documented); + detailOutput.header("Documentation Generator"); + print.line("Generating documentation...").toConsole(); + detailOutput.line(); + + var outputPath = resolvePath(arguments.output); + var componentsToDocument = listToArray(arguments.include); - if (arrayLen(documented) > 0) { - detailOutput.statusSuccess("Found #arrayLen(documented)# #componentType#"); - } else if (arguments.verbose) { - detailOutput.statusWarning("No #componentType# found"); + // Ensure output directory exists + if (!directoryExists(outputPath)) { + directoryCreate(outputPath, true); + detailOutput.create("directory #arguments.output#"); } - } - - // Generate index/navigation - detailOutput.line(); - print.line("Writing documentation...").toConsole(); - - if (arguments.format == "html") { - generateHTMLIndex(outputPath, documentedComponents, arguments.template); - detailOutput.statusSuccess("HTML files generated"); - } else if (arguments.format == "markdown") { - generateMarkdownIndex(outputPath, documentedComponents); - detailOutput.statusSuccess("Markdown files generated"); - } else if (arguments.format == "json") { - fileWrite(outputPath & "/documentation.json", serializeJSON(documentedComponents, true)); - detailOutput.statusSuccess("JSON documentation generated"); - } - - // Display summary - detailOutput.line(); - detailOutput.statusSuccess("Documentation generated successfully!"); - detailOutput.line(); - - detailOutput.subHeader("Summary"); - for (var type in componentsToDocument) { - if (arrayLen(documentedComponents[type])) { - detailOutput.metric( - label = "#uCase(left(type, 1)) & right(type, len(type)-1)#", - value = "#arrayLen(documentedComponents[type])# files" + + var documentedComponents = { + models = [], + controllers = [], + views = [], + services = [], + total = 0 + }; + + detailOutput.subHeader("Scanning Source Files"); + + // Document each component type + for (var componentType in componentsToDocument) { + if (arguments.verbose) { + print.line("Documenting #componentType#...").toConsole(); + } + + var documented = documentComponents( + type = componentType, + outputPath = outputPath, + format = arguments.format, + template = arguments.template, + verbose = arguments.verbose ); + + documentedComponents[componentType] = documented; + documentedComponents.total += arrayLen(documented); + + if (arrayLen(documented) > 0) { + detailOutput.statusSuccess("Found #arrayLen(documented)# #componentType#"); + } else if (arguments.verbose) { + detailOutput.statusWarning("No #componentType# found"); + } } - } - detailOutput.metric( - label = "Total Components", - value = "#documentedComponents.total# documented" - ); - detailOutput.line(); - detailOutput.statusInfo("Output directory: #outputPath#"); - - if (arguments.serve) { + + // Generate index/navigation + detailOutput.line(); + print.line("Writing documentation...").toConsole(); + + if (arguments.format == "html") { + generateHTMLIndex(outputPath, documentedComponents, arguments.template); + detailOutput.statusSuccess("HTML files generated"); + } else if (arguments.format == "markdown") { + generateMarkdownIndex(outputPath, documentedComponents); + detailOutput.statusSuccess("Markdown files generated"); + } else if (arguments.format == "json") { + fileWrite(outputPath & "/documentation.json", serializeJSON(documentedComponents, true)); + detailOutput.statusSuccess("JSON documentation generated"); + } + + // Display summary detailOutput.line(); - print.line("Starting documentation server...").toConsole(); + detailOutput.statusSuccess("Documentation generated successfully!"); detailOutput.line(); - // Start server using CommandBox - command("wheels docs serve") - .params( - directory = outputPath, - port = 8585, - browser = true - ) - .run(); + detailOutput.subHeader("Summary"); + for (var type in componentsToDocument) { + if (arrayLen(documentedComponents[type])) { + detailOutput.metric( + label = "#uCase(left(type, 1)) & right(type, len(type)-1)#", + value = "#arrayLen(documentedComponents[type])# files" + ); + } + } + detailOutput.metric( + label = "Total Components", + value = "#documentedComponents.total# documented" + ); + detailOutput.line(); + detailOutput.statusInfo("Output directory: #outputPath#"); - detailOutput.statusSuccess("Documentation server started at http://localhost:8585"); - } + if (arguments.serve) { + detailOutput.line(); + print.line("Starting documentation server...").toConsole(); + detailOutput.line(); + + // Start server using CommandBox + command("wheels docs serve") + .params( + directory = outputPath, + port = 8585, + browser = true + ) + .run(); + + detailOutput.statusSuccess("Documentation server started at http://localhost:8585"); + } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } private function documentComponents( diff --git a/cli/src/commands/wheels/env/merge.cfc b/cli/src/commands/wheels/env/merge.cfc index 53f97e1377..1ca16942f2 100644 --- a/cli/src/commands/wheels/env/merge.cfc +++ b/cli/src/commands/wheels/env/merge.cfc @@ -24,60 +24,65 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { string output = ".env.merge", boolean dryRun = false ) { - requireWheelsApp(getCWD()); - // Reconstruct arguments to handle -- prefixed options - arguments = reconstructArgs(arguments); - local.sourceFiles = [arguments.source1, arguments.source2]; - - // Check for additional positional arguments (source3, source4, etc.) - local.i = 3; - while (StructKeyExists(arguments, "source" & local.i)) { - ArrayAppend(local.sourceFiles, arguments["source" & local.i]); - local.i++; - } - - if (ArrayLen(local.sourceFiles) < 2) { - detailOutput.error("At least two source files are required. Usage: wheels env merge file1 file2 [--output=filename] [--dryRun]"); - return; - } + try { + requireWheelsApp(getCWD()); + // Reconstruct arguments to handle -- prefixed options + arguments = reconstructArgs(arguments); + local.sourceFiles = [arguments.source1, arguments.source2]; + + // Check for additional positional arguments (source3, source4, etc.) + local.i = 3; + while (StructKeyExists(arguments, "source" & local.i)) { + ArrayAppend(local.sourceFiles, arguments["source" & local.i]); + local.i++; + } - // Validate all source files exist - for (local.file in local.sourceFiles) { - if (!FileExists(ResolvePath(local.file))) { - detailOutput.error("Source file not found: #local.file#"); + if (ArrayLen(local.sourceFiles) < 2) { + detailOutput.error("At least two source files are required. Usage: wheels env merge file1 file2 [--output=filename] [--dryRun]"); return; } - } - - print.line("Merging environment files...").toConsole(); - detailOutput.line(); - detailOutput.subHeader("Source Files"); - for (local.i = 1; local.i <= ArrayLen(local.sourceFiles); local.i++) { - detailOutput.metric("#local.i#.", local.sourceFiles[local.i]); - } - detailOutput.line(); - // Merge the files - local.merged = mergeEnvFiles(local.sourceFiles); + // Validate all source files exist + for (local.file in local.sourceFiles) { + if (!FileExists(ResolvePath(local.file))) { + detailOutput.error("Source file not found: #local.file#"); + return; + } + } - // Display the result - if (arguments.dryRun) { - displayMergedResult(local.merged, true); - } else { - // Write the merged file - writeMergedFile(arguments.output, local.merged); + print.line("Merging environment files...").toConsole(); detailOutput.line(); - detailOutput.statusSuccess("Merged #ArrayLen(local.sourceFiles)# files into #arguments.output#"); - detailOutput.metric("Total variables", "#StructCount(local.merged.vars)#"); - - // Show conflicts if any - if (ArrayLen(local.merged.conflicts)) { + detailOutput.subHeader("Source Files"); + for (local.i = 1; local.i <= ArrayLen(local.sourceFiles); local.i++) { + detailOutput.metric("#local.i#.", local.sourceFiles[local.i]); + } + detailOutput.line(); + + // Merge the files + local.merged = mergeEnvFiles(local.sourceFiles); + + // Display the result + if (arguments.dryRun) { + displayMergedResult(local.merged, true); + } else { + // Write the merged file + writeMergedFile(arguments.output, local.merged); detailOutput.line(); - detailOutput.statusWarning("Conflicts resolved (later files take precedence):"); - for (local.conflict in local.merged.conflicts) { - detailOutput.output(" - #local.conflict#", true); + detailOutput.statusSuccess("Merged #ArrayLen(local.sourceFiles)# files into #arguments.output#"); + detailOutput.metric("Total variables", "#StructCount(local.merged.vars)#"); + + // Show conflicts if any + if (ArrayLen(local.merged.conflicts)) { + detailOutput.line(); + detailOutput.statusWarning("Conflicts resolved (later files take precedence):"); + for (local.conflict in local.merged.conflicts) { + detailOutput.output(" - #local.conflict#", true); + } } } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } } diff --git a/cli/src/commands/wheels/env/set.cfc b/cli/src/commands/wheels/env/set.cfc index 12ed06cd86..c92172000e 100644 --- a/cli/src/commands/wheels/env/set.cfc +++ b/cli/src/commands/wheels/env/set.cfc @@ -19,26 +19,30 @@ component extends="commandbox.modules.wheels-cli.commands.wheels.base" { **/ function run( string file = ".env" - ) - { - requireWheelsApp(getCWD()); - arguments = reconstructArgs(argStruct=arguments); - local.updates = {}; - - for (local.key in arguments) { - // Skip reserved keywords like "file" - if (local.key != "file") { - local.updates[local.key] = arguments[local.key]; + ) { + try { + requireWheelsApp(getCWD()); + arguments = reconstructArgs(argStruct=arguments); + local.updates = {}; + + for (local.key in arguments) { + // Skip reserved keywords like "file" + if (local.key != "file") { + local.updates[local.key] = arguments[local.key]; + } } - } - if (StructIsEmpty(local.updates)) { - detailOutput.error("No key=value pairs provided. Usage: wheels env set KEY=VALUE"); - } + if (StructIsEmpty(local.updates)) { + detailOutput.error("No key=value pairs provided. Usage: wheels env set KEY=VALUE"); + } - // Update the .env file - local.envFile = ResolvePath(arguments.file); - updateEnvFile(local.envFile, local.updates); + // Update the .env file + local.envFile = ResolvePath(arguments.file); + updateEnvFile(local.envFile, local.updates); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } diff --git a/cli/src/commands/wheels/init.cfc b/cli/src/commands/wheels/init.cfc index b344ed559a..827c57fee6 100644 --- a/cli/src/commands/wheels/init.cfc +++ b/cli/src/commands/wheels/init.cfc @@ -18,89 +18,92 @@ component extends="base" { * **/ function run() { + try { + requireWheelsApp(getCWD()); + detailOutput.header("Wheels init") + .output("This function will attempt to add a few things") + .output("to an EXISTING Wheels installation to help") + .output("the CLI interact.") + .line() + .output("We're going to assume the following:") + .output("- you've already setup a local datasource/database", true) + .output("- you've already set a reload password", true) + .line() + .output("We're going to try and do the following:") + .output("- create a box.json to help keep track of the wheels version", true) + .output("- create a server.json", true) + .divider() + .line(); + + if(!confirm("Sound ok? [y/n] ")){ + detailOutput.getPrint().redBoldLine("Ok, aborting...").toConsole(); + return; + } - requireWheelsApp(getCWD()); - detailOutput.header("Wheels init") - .output("This function will attempt to add a few things") - .output("to an EXISTING Wheels installation to help") - .output("the CLI interact.") - .line() - .output("We're going to assume the following:") - .output("- you've already setup a local datasource/database", true) - .output("- you've already set a reload password", true) - .line() - .output("We're going to try and do the following:") - .output("- create a box.json to help keep track of the wheels version", true) - .output("- create a server.json", true) - .divider() - .line(); - - if(!confirm("Sound ok? [y/n] ")){ - detailOutput.getPrint().redBoldLine("Ok, aborting...").toConsole(); - return; - } - - var serverJsonLocation=fileSystemUtil.resolvePath("server.json"); - var wheelsBoxJsonLocation=fileSystemUtil.resolvePath("vendor/wheels/box.json"); - var boxJsonLocation=fileSystemUtil.resolvePath("box.json"); - - var wheelsVersion = $getWheelsVersion(); - detailOutput.statusInfo(wheelsVersion); - - // Create a wheels/box.json if one doesn't exist - if(!fileExists(wheelsBoxJsonLocation)){ - var wheelsBoxJSON = fileRead( getTemplate('/WheelsBoxJSON.txt' ) ); - wheelsBoxJSON = replaceNoCase( wheelsBoxJSON, "|version|", trim(wheelsVersion), 'all' ); - - // Make box.json - detailOutput.statusInfo("Creating wheels/box.json"); - file action='write' file=wheelsBoxJsonLocation mode ='777' output='#trim(wheelsBoxJSON)#'; - detailOutput.create(wheelsBoxJsonLocation); - detailOutput.statusSuccess("Created wheels/box.json"); - - } else { - detailOutput.statusInfo("wheels/box.json exists, skipping"); - } + var serverJsonLocation=fileSystemUtil.resolvePath("server.json"); + var wheelsBoxJsonLocation=fileSystemUtil.resolvePath("vendor/wheels/box.json"); + var boxJsonLocation=fileSystemUtil.resolvePath("box.json"); - // Create a server.json if one doesn't exist - if(!fileExists(serverJsonLocation)){ - var appName = ask( message = "Please enter an application name (we use this to make the server.json servername unique): ", defaultResponse = 'myapp'); - appName = helpers.stripSpecialChars(appName); - var setEngine = ask( message = 'Please enter a default cfengine: ', defaultResponse = 'lucee@6' ); + var wheelsVersion = $getWheelsVersion(); + detailOutput.statusInfo(wheelsVersion); - // Make server.json server name unique to this app: assumes lucee by default - detailOutput.statusInfo("Creating default server.json"); - var serverJSON = fileRead( getTemplate('/ServerJSON.txt' ) ); - serverJSON = replaceNoCase( serverJSON, "|appName|", trim(appName), 'all' ); - serverJSON = replaceNoCase( serverJSON, "|setEngine|", setEngine, 'all' ); - file action='write' file=serverJsonLocation mode ='777' output='#trim(serverJSON)#'; - detailOutput.create(serverJsonLocation); - detailOutput.statusSuccess("Created server.json"); + // Create a wheels/box.json if one doesn't exist + if(!fileExists(wheelsBoxJsonLocation)){ + var wheelsBoxJSON = fileRead( getTemplate('/WheelsBoxJSON.txt' ) ); + wheelsBoxJSON = replaceNoCase( wheelsBoxJSON, "|version|", trim(wheelsVersion), 'all' ); - } else { - detailOutput.statusInfo("server.json exists, skipping"); - } + // Make box.json + detailOutput.statusInfo("Creating wheels/box.json"); + file action='write' file=wheelsBoxJsonLocation mode ='777' output='#trim(wheelsBoxJSON)#'; + detailOutput.create(wheelsBoxJsonLocation); + detailOutput.statusSuccess("Created wheels/box.json"); - // Create a box.json if one doesn't exist - if(!fileExists(boxJsonLocation)){ - if(!isDefined("appName")) { - var appName = ask("Please enter an application name (we use this to make the box.json servername unique): "); - appName = helpers.stripSpecialChars(appName); + } else { + detailOutput.statusInfo("wheels/box.json exists, skipping"); } - var boxJSON = fileRead( getTemplate('/BoxJSON.txt' ) ); - boxJSON = replaceNoCase( boxJSON, "|version|", trim(wheelsVersion), 'all' ); - boxJSON = replaceNoCase( boxJSON, "|appName|", trim(appName), 'all' ); - // Make box.json - detailOutput.statusInfo("Creating box.json"); - file action='write' file=boxJsonLocation mode ='777' output='#trim(boxJSON)#'; - detailOutput.create(boxJsonLocation); - detailOutput.statusSuccess("Created box.json"); + // Create a server.json if one doesn't exist + if(!fileExists(serverJsonLocation)){ + var appName = ask( message = "Please enter an application name (we use this to make the server.json servername unique): ", defaultResponse = 'myapp'); + appName = helpers.stripSpecialChars(appName); + var setEngine = ask( message = 'Please enter a default cfengine: ', defaultResponse = 'lucee@6' ); + + // Make server.json server name unique to this app: assumes lucee by default + detailOutput.statusInfo("Creating default server.json"); + var serverJSON = fileRead( getTemplate('/ServerJSON.txt' ) ); + serverJSON = replaceNoCase( serverJSON, "|appName|", trim(appName), 'all' ); + serverJSON = replaceNoCase( serverJSON, "|setEngine|", setEngine, 'all' ); + file action='write' file=serverJsonLocation mode ='777' output='#trim(serverJSON)#'; + detailOutput.create(serverJsonLocation); + detailOutput.statusSuccess("Created server.json"); + + } else { + detailOutput.statusInfo("server.json exists, skipping"); + } - } else { - detailOutput.statusInfo("box.json exists, skipping"); + // Create a box.json if one doesn't exist + if(!fileExists(boxJsonLocation)){ + if(!isDefined("appName")) { + var appName = ask("Please enter an application name (we use this to make the box.json servername unique): "); + appName = helpers.stripSpecialChars(appName); + } + var boxJSON = fileRead( getTemplate('/BoxJSON.txt' ) ); + boxJSON = replaceNoCase( boxJSON, "|version|", trim(wheelsVersion), 'all' ); + boxJSON = replaceNoCase( boxJSON, "|appName|", trim(appName), 'all' ); + + // Make box.json + detailOutput.statusInfo("Creating box.json"); + file action='write' file=boxJsonLocation mode ='777' output='#trim(boxJSON)#'; + detailOutput.create(boxJsonLocation); + detailOutput.statusSuccess("Created box.json"); + + } else { + detailOutput.statusInfo("box.json exists, skipping"); + } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } - } } \ No newline at end of file diff --git a/cli/src/commands/wheels/plugins/list.cfc b/cli/src/commands/wheels/plugins/list.cfc index 14d85e06fe..bcfad0f0eb 100644 --- a/cli/src/commands/wheels/plugins/list.cfc +++ b/cli/src/commands/wheels/plugins/list.cfc @@ -19,224 +19,229 @@ component aliases="wheels plugin list" extends="../base" { string format = "table", boolean available = false ) { - requireWheelsApp(getCWD()); - arguments = reconstructArgs( - argStruct=arguments, - allowedValues={ - format=["table", "json"] - } - ); - - if (arguments.available) { - // Show available plugins from ForgeBox - detailOutput.header("Available Wheels Plugins on ForgeBox"); - detailOutput.output("Searching, please wait..."); - detailOutput.line(); - - // Get list of all cfwheels plugins slugs - var forgeboxResult = command('forgebox show') - .params(type='cfwheels-plugins') - .run(returnOutput=true); - - var results = []; - - if (len(forgeboxResult)) { - var lines = listToArray(forgeboxResult, chr(10) & chr(13)); - - for (var i = 1; i <= arrayLen(lines); i++) { - var line = trim(lines[i]); - - // Check if this is a slug line: Slug: "slug-name" - if (findNoCase('Slug:', line)) { - // Extract slug from quotes - var slugMatch = reFind('Slug:\s*"([^"]+)"', line, 1, true); - if (slugMatch.pos[1] > 0) { - var slug = mid(line, slugMatch.pos[2], slugMatch.len[2]); - - try { - var pluginInfo = forgebox.getEntry(slug); - - if (isStruct(pluginInfo) && structKeyExists(pluginInfo, "slug")) { - // Extract version from latestVersion structure - var version = "N/A"; - if (structKeyExists(pluginInfo, "latestVersion") && - isStruct(pluginInfo.latestVersion) && - structKeyExists(pluginInfo.latestVersion, "version")) { - version = pluginInfo.latestVersion.version; - } + try { + requireWheelsApp(getCWD()); + arguments = reconstructArgs( + argStruct=arguments, + allowedValues={ + format=["table", "json"] + } + ); - // Extract author from user structure - var author = "Unknown"; - if (structKeyExists(pluginInfo, "user") && - isStruct(pluginInfo.user) && - structKeyExists(pluginInfo.user, "username")) { - author = pluginInfo.user.username; - } + if (arguments.available) { + // Show available plugins from ForgeBox + detailOutput.header("Available Wheels Plugins on ForgeBox"); + detailOutput.output("Searching, please wait..."); + detailOutput.line(); - arrayAppend(results, { - name: pluginInfo.title ?: slug, - slug: slug, - version: version, - description: pluginInfo.summary ?: pluginInfo.description ?: "", - author: author, - downloads: pluginInfo.hits ?: 0, - updateDate: pluginInfo.updatedDate ?: "" - }); + // Get list of all cfwheels plugins slugs + var forgeboxResult = command('forgebox show') + .params(type='cfwheels-plugins') + .run(returnOutput=true); + + var results = []; + + if (len(forgeboxResult)) { + var lines = listToArray(forgeboxResult, chr(10) & chr(13)); + + for (var i = 1; i <= arrayLen(lines); i++) { + var line = trim(lines[i]); + + // Check if this is a slug line: Slug: "slug-name" + if (findNoCase('Slug:', line)) { + // Extract slug from quotes + var slugMatch = reFind('Slug:\s*"([^"]+)"', line, 1, true); + if (slugMatch.pos[1] > 0) { + var slug = mid(line, slugMatch.pos[2], slugMatch.len[2]); + + try { + var pluginInfo = forgebox.getEntry(slug); + + if (isStruct(pluginInfo) && structKeyExists(pluginInfo, "slug")) { + // Extract version from latestVersion structure + var version = "N/A"; + if (structKeyExists(pluginInfo, "latestVersion") && + isStruct(pluginInfo.latestVersion) && + structKeyExists(pluginInfo.latestVersion, "version")) { + version = pluginInfo.latestVersion.version; + } + + // Extract author from user structure + var author = "Unknown"; + if (structKeyExists(pluginInfo, "user") && + isStruct(pluginInfo.user) && + structKeyExists(pluginInfo.user, "username")) { + author = pluginInfo.user.username; + } + + arrayAppend(results, { + name: pluginInfo.title ?: slug, + slug: slug, + version: version, + description: pluginInfo.summary ?: pluginInfo.description ?: "", + author: author, + downloads: pluginInfo.hits ?: 0, + updateDate: pluginInfo.updatedDate ?: "" + }); + } + } catch (any e) { + // Skip plugins that can't be retrieved } - } catch (any e) { - // Skip plugins that can't be retrieved } } } } + + results.sort(function(a, b) { + return compareNoCase(a.name, b.name); + }); + + if (arguments.format == "json") { + var jsonOutput = { + "plugins": results, + "count": arrayLen(results) + }; + print.line(jsonOutput).toConsole(); + } else { + detailOutput.subHeader("Found #arrayLen(results)# plugin(s)"); + detailOutput.line(); + + // Create table for results + var rows = []; + + for (var plugin in results) { + // use ordered struct so JSON keeps key order + var row = structNew("ordered"); + + row["Name"] = plugin.name; + row["Slug"] = plugin.slug; + row["Version"] = plugin.version; + row["Downloads"] = numberFormat(plugin.downloads ?: 0); + row["Description"] = plugin.description ?: "No description"; + + // Truncate long descriptions + if (len(row["Description"]) > 50) { + row["Description"] = left(row["Description"], 47) & "..."; + } + + arrayAppend(rows, row); + } + + // Display the table + detailOutput.getPrint().table(rows).toConsole(); + + detailOutput.line(); + detailOutput.divider(); + detailOutput.line(); + + // Show summary + detailOutput.metric("Total plugins found", "#arrayLen(results)#"); + detailOutput.line(); + + // Show commands + detailOutput.subHeader("Commands"); + detailOutput.output("- Install: wheels plugin install ", true); + detailOutput.output("- Details: wheels plugin info ", true); + detailOutput.output("- Add --format=json for JSON output", true); + detailOutput.line(); + } + return; } - results.sort(function(a, b) { - return compareNoCase(a.name, b.name); - }); + // Show installed plugins + var plugins = pluginService.list(); + + if (arrayLen(plugins) == 0) { + detailOutput.header("Installed Wheels Plugins"); + detailOutput.line(); + detailOutput.statusWarning("No plugins installed in /plugins folder"); + detailOutput.line(); + detailOutput.subHeader("Install plugins with"); + detailOutput.output("- wheels plugin install ", true); + detailOutput.line(); + detailOutput.subHeader("See available plugins"); + detailOutput.output("- wheels plugin list --available", true); + detailOutput.output("- wheels plugin search ", true); + return; + } if (arguments.format == "json") { + // JSON format output var jsonOutput = { - "plugins": results, - "count": arrayLen(results) + "plugins": plugins, + "count": arrayLen(plugins) }; print.line(jsonOutput).toConsole(); } else { - detailOutput.subHeader("Found #arrayLen(results)# plugin(s)"); - detailOutput.line(); + // Table format output + detailOutput.header("Installed Wheels Plugins (#arrayLen(plugins)#)"); - // Create table for results + // Create table rows var rows = []; - - for (var plugin in results) { - // use ordered struct so JSON keeps key order - var row = structNew("ordered"); - - row["Name"] = plugin.name; - row["Slug"] = plugin.slug; - row["Version"] = plugin.version; - row["Downloads"] = numberFormat(plugin.downloads ?: 0); - row["Description"] = plugin.description ?: "No description"; - - // Truncate long descriptions - if (len(row["Description"]) > 50) { - row["Description"] = left(row["Description"], 47) & "..."; + for (var plugin in plugins) { + var row = { + "Plugin Name": plugin.name, + "Slug" :plugin.slug, + "Version": plugin.version + }; + + if (plugin.keyExists("description") && len(plugin.description)) { + row["Description"] = left(plugin.description, 50); + } else { + row["Description"] = ""; } - + + // Add author if available + if (plugin.keyExists("author") && len(plugin.author)) { + row["Author"] = left(plugin.author, 20); + } + arrayAppend(rows, row); } // Display the table - detailOutput.getPrint().table(rows).toConsole(); + detailOutput.getPrint().table(rows); detailOutput.line(); - detailOutput.divider(); + detailOutput.divider("-", 60); detailOutput.line(); // Show summary - detailOutput.metric("Total plugins found", "#arrayLen(results)#"); + detailOutput.metric("Total plugins", "#arrayLen(plugins)#"); + var devPlugins = 0; + for (var plugin in plugins) { + if (plugin.keyExists("type") && findNoCase("dev", plugin.type)) { + devPlugins++; + } + } + if (devPlugins > 0) { + detailOutput.metric("Development plugins", "#devPlugins#"); + } + + // Show most recent plugin if available + if (arrayLen(plugins) > 0) { + var recentPlugin = plugins[1]; // Assuming first is most recent + detailOutput.metric("Latest plugin", "#recentPlugin.name# (#recentPlugin.version#)"); + } + detailOutput.line(); // Show commands detailOutput.subHeader("Commands"); - detailOutput.output("- Install: wheels plugin install ", true); - detailOutput.output("- Details: wheels plugin info ", true); - detailOutput.output("- Add --format=json for JSON output", true); + detailOutput.output("- wheels plugin info View plugin details", true); + detailOutput.output("- wheels plugin update:all Update all plugins", true); + detailOutput.output("- wheels plugin outdated Check for updates", true); + detailOutput.output("- wheels plugin install Install new plugin", true); + detailOutput.output("- wheels plugin remove Remove a plugin", true); detailOutput.line(); - } - return; - } - - // Show installed plugins - var plugins = pluginService.list(); - - if (arrayLen(plugins) == 0) { - detailOutput.header("Installed Wheels Plugins"); - detailOutput.line(); - detailOutput.statusWarning("No plugins installed in /plugins folder"); - detailOutput.line(); - detailOutput.subHeader("Install plugins with"); - detailOutput.output("- wheels plugin install ", true); - detailOutput.line(); - detailOutput.subHeader("See available plugins"); - detailOutput.output("- wheels plugin list --available", true); - detailOutput.output("- wheels plugin search ", true); - return; - } - - if (arguments.format == "json") { - // JSON format output - var jsonOutput = { - "plugins": plugins, - "count": arrayLen(plugins) - }; - print.line(jsonOutput).toConsole(); - } else { - // Table format output - detailOutput.header("Installed Wheels Plugins (#arrayLen(plugins)#)"); - - // Create table rows - var rows = []; - for (var plugin in plugins) { - var row = { - "Plugin Name": plugin.name, - "Slug" :plugin.slug, - "Version": plugin.version - }; - - if (plugin.keyExists("description") && len(plugin.description)) { - row["Description"] = left(plugin.description, 50); - } else { - row["Description"] = ""; - } - - // Add author if available - if (plugin.keyExists("author") && len(plugin.author)) { - row["Author"] = left(plugin.author, 20); - } - arrayAppend(rows, row); - } - - // Display the table - detailOutput.getPrint().table(rows); - - detailOutput.line(); - detailOutput.divider("-", 60); - detailOutput.line(); - - // Show summary - detailOutput.metric("Total plugins", "#arrayLen(plugins)#"); - var devPlugins = 0; - for (var plugin in plugins) { - if (plugin.keyExists("type") && findNoCase("dev", plugin.type)) { - devPlugins++; - } - } - if (devPlugins > 0) { - detailOutput.metric("Development plugins", "#devPlugins#"); - } - - // Show most recent plugin if available - if (arrayLen(plugins) > 0) { - var recentPlugin = plugins[1]; // Assuming first is most recent - detailOutput.metric("Latest plugin", "#recentPlugin.name# (#recentPlugin.version#)"); + // Add tip + detailOutput.statusInfo("Tip"); + detailOutput.output("Add --format=json for JSON output", true); } - - detailOutput.line(); - - // Show commands - detailOutput.subHeader("Commands"); - detailOutput.output("- wheels plugin info View plugin details", true); - detailOutput.output("- wheels plugin update:all Update all plugins", true); - detailOutput.output("- wheels plugin outdated Check for updates", true); - detailOutput.output("- wheels plugin install Install new plugin", true); - detailOutput.output("- wheels plugin remove Remove a plugin", true); - detailOutput.line(); - - // Add tip - detailOutput.statusInfo("Tip"); - detailOutput.output("Add --format=json for JSON output", true); - } + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); + } } } \ No newline at end of file diff --git a/cli/src/commands/wheels/reload.cfc b/cli/src/commands/wheels/reload.cfc index df15aac36d..69c0f847c6 100644 --- a/cli/src/commands/wheels/reload.cfc +++ b/cli/src/commands/wheels/reload.cfc @@ -21,38 +21,43 @@ component aliases='wheels r' extends="base" { property name="detailOutput" inject="DetailOutputService@wheels-cli"; function run(string mode="development", string password="") { - requireWheelsApp(getCWD()); - arguments=reconstructArgs(arguments); - var serverDetails = $getServerInfo(); - var appSettings = $getAppSettings(mode); + try { + requireWheelsApp(getCWD()); + arguments=reconstructArgs(arguments); + var serverDetails = $getServerInfo(); + var appSettings = $getAppSettings(mode); - var reloadPassword = StructKeyExists(appSettings, "reloadPassword") ? appSettings.reloadPassword : ""; - getURL = serverDetails.serverURL & "/index.cfm?reload=#mode#"; + var reloadPassword = StructKeyExists(appSettings, "reloadPassword") ? appSettings.reloadPassword : ""; + getURL = serverDetails.serverURL & "/index.cfm?reload=#mode#"; - // Handle password logic - if (len(reloadPassword)) { - // Password is configured - if (len(password)) { - // User provided a password, validate it against configured one - if (password != reloadPassword) { - detailOutput.error("Invalid password. The configured reload password does not match the provided password."); + // Handle password logic + if (len(reloadPassword)) { + // Password is configured + if (len(password)) { + // User provided a password, validate it against configured one + if (password != reloadPassword) { + detailOutput.error("Invalid password. The configured reload password does not match the provided password."); + return; + } + getURL &= "&password=#password#"; + } else { + detailOutput.error("Reload password is configured but not provided!"); return; } - getURL &= "&password=#password#"; } else { - detailOutput.error("Reload password is configured but not provided!"); - return; - } - } else { - // No password configured - check if user provided one unnecessarily - if (len(password)) { - detailOutput.statusWarning("No reload password is configured in settings, but you provided one. Proceeding without password."); + // No password configured - check if user provided one unnecessarily + if (len(password)) { + detailOutput.statusWarning("No reload password is configured in settings, but you provided one. Proceeding without password."); + } } + getURL = serverDetails.serverURL & + "/index.cfm?reload=#mode#&password=#password#"; + var loc = new Http( url=getURL ).send().getPrefix(); + detailOutput.statusSuccess("Reload Request sent"); + } catch (any e) { + detailOutput.error("#e.message#"); + setExitCode(1); } - getURL = serverDetails.serverURL & - "/index.cfm?reload=#mode#&password=#password#"; - var loc = new Http( url=getURL ).send().getPrefix(); - detailOutput.statusSuccess("Reload Request sent"); } private struct function $getAppSettings(required string mode="development") { From 6c9e25e46c60bf9b45fdfed100bff0b6f3c56277 Mon Sep 17 00:00:00 2001 From: zainforbjs Date: Wed, 11 Feb 2026 18:08:19 +0500 Subject: [PATCH 050/405] Fix adobe 2025 bugs for starter App Updated the User.cfc file and used correct argument types. Updated the Model.cfc and replaced the use of htmleditFormat as it does not exist in adobe 2025 --- examples/starter-app/app/models/Model.cfc | 11 ++++++++--- examples/starter-app/app/models/User.cfc | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/starter-app/app/models/Model.cfc b/examples/starter-app/app/models/Model.cfc index a28ff2722e..991bb24c80 100644 --- a/examples/starter-app/app/models/Model.cfc +++ b/examples/starter-app/app/models/Model.cfc @@ -38,9 +38,14 @@ component extends="wheels.Model" { * Simple sanitization: this could probably be improved somewhat. **/ private function sanitizeInput(string){ - local.rv = REReplaceNoCase(arguments.string, "<\ *[a-z].*?>", "", "all"); - local.rv = REReplaceNoCase(local.rv, "<\ */\ *[a-z].*?>", "", "all"); - local.rv = trim(htmleditFormat(local.rv)); + local.rv = reReplaceNoCase(arguments.string, "<[^>]*>", "", "all"); + local.rv = trim(local.rv); + + local.rv = replace(local.rv, "&", "&", "all"); + local.rv = replace(local.rv, "<", "<", "all"); + local.rv = replace(local.rv, ">", ">", "all"); + local.rv = replace(local.rv, '"', """, "all"); + return local.rv; } diff --git a/examples/starter-app/app/models/User.cfc b/examples/starter-app/app/models/User.cfc index 68e17281d5..44e4036d46 100644 --- a/examples/starter-app/app/models/User.cfc +++ b/examples/starter-app/app/models/User.cfc @@ -115,7 +115,7 @@ component extends="Model" { return Replace(LCase(CreateUUID()), "-", "", "all"); } - function getUsers(required array where, required bool includeSoftDeletes, required number page, required number perpage) { + function getUsers(required array where, required boolean includeSoftDeletes, required numeric page, required numeric perpage) { return findAll(where=whereify(arguments.where), page=arguments.page, includeSoftDeletes=arguments.includeSoftDeletes, perpage=arguments.perpage, include="role"); } From e31e8f34e4c873d87bc069f251067913722db157 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 11 Feb 2026 15:02:38 -0800 Subject: [PATCH 051/405] Modernize AI infrastructure: consolidate skills, commands, and CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce AI tooling from ~45,000 lines across 160+ files to a lean configuration that trusts Claude Code's native search capabilities instead of force-feeding docs. - Rewrite CLAUDE.md: 1,034 → 220 lines with top 10 anti-patterns inline - Consolidate skills: 15 → 5 (codegen, testing, scaffold, refactoring, deployment) - Replace monolithic command: 1×2,311 lines → 3 focused commands (spec, build, validate) - Delete 34 per-directory CLAUDE.md dispatcher files from templates/examples/tests - Delete .ai/ meta-instruction files and workflows (reference docs preserved) - Clean settings.local.json: remove puppeteer, stale paths, redundant entries - Delete .opencode/ directory (duplicate of .claude/) Co-Authored-By: Claude Opus 4.6 --- .ai/CLAUDE.md | 179 -- .ai/CONTRIBUTION_SUMMARY.md | 162 -- .ai/MCP-ENFORCEMENT.md | 70 - .ai/QUICK_REFERENCE.md | 244 -- .ai/README.md | 173 +- .../documentation-loading-strategy.md | 510 ---- .../enhanced-mcp-develop-specification.md | 480 ---- .../intelligent-analysis-planning-engine.md | 1009 ------- .ai/wheels/workflows/pre-implementation.md | 429 --- ...template-driven-implementation-patterns.md | 849 ------ .claude/commands/wheels_build.md | 370 +++ .claude/commands/wheels_execute.md | 2312 ----------------- .claude/commands/wheels_spec.md | 226 ++ .claude/commands/wheels_validate.md | 211 ++ .claude/settings.local.json | 32 +- .claude/skills/README.md | 502 ---- .claude/skills/SKILLS-QUICK-START.md | 104 - .../wheels-anti-pattern-detector/SKILL.md | 576 ---- .claude/skills/wheels-api-generator/SKILL.md | 138 - .claude/skills/wheels-auth-generator/SKILL.md | 243 -- .claude/skills/wheels-codegen/SKILL.md | 739 ++++++ .../templates/basic-model.cfc | 0 .../templates/user-authentication-model.cfc | 0 .../wheels-controller-generator/SKILL.md | 674 ----- .claude/skills/wheels-debugging/SKILL.md | 137 - .claude/skills/wheels-deployment/SKILL.md | 3 - .../wheels-documentation-generator/SKILL.md | 82 - .../skills/wheels-email-generator/SKILL.md | 616 ----- .../wheels-migration-generator/SKILL.md | 709 ----- .../skills/wheels-model-generator/SKILL.md | 750 ------ .../skills/wheels-plugin-generator/SKILL.md | 459 ---- .claude/skills/wheels-refactoring/SKILL.md | 3 - .../skills/wheels-routing-generator/SKILL.md | 632 ----- .claude/skills/wheels-scaffold/SKILL.md | 443 ++++ .claude/skills/wheels-test-generator/SKILL.md | 281 -- .claude/skills/wheels-testing/SKILL.md | 308 +++ .claude/skills/wheels-view-generator/SKILL.md | 703 ----- .opencode/command/wheels_execute.md | 619 ----- CLAUDE.md | 1095 +------- examples/tweet/.ai/CLAUDE.md | 179 -- examples/tweet/CLAUDE.md | 2074 --------------- examples/tweet/app/CLAUDE.md | 57 - examples/tweet/app/controllers/CLAUDE.md | 46 - examples/tweet/app/events/CLAUDE.md | 20 - examples/tweet/app/global/CLAUDE.md | 18 - examples/tweet/app/jobs/CLAUDE.md | 19 - examples/tweet/app/lib/CLAUDE.md | 20 - examples/tweet/app/mailers/CLAUDE.md | 20 - examples/tweet/app/migrator/CLAUDE.md | 25 - examples/tweet/app/models/CLAUDE.md | 49 - examples/tweet/app/snippets/CLAUDE.md | 27 - examples/tweet/app/views/CLAUDE.md | 59 - examples/tweet/config/CLAUDE.md | 842 ------ examples/tweet/plugins/CLAUDE.md | 906 ------- examples/tweet/public/CLAUDE.md | 753 ------ examples/tweet/tests/CLAUDE.md | 1362 ---------- templates/base/src/CLAUDE.md | 547 ---- templates/base/src/app/CLAUDE.md | 57 - templates/base/src/app/controllers/CLAUDE.md | 46 - templates/base/src/app/events/CLAUDE.md | 20 - templates/base/src/app/global/CLAUDE.md | 18 - templates/base/src/app/jobs/CLAUDE.md | 19 - templates/base/src/app/lib/CLAUDE.md | 20 - templates/base/src/app/mailers/CLAUDE.md | 20 - templates/base/src/app/migrator/CLAUDE.md | 25 - templates/base/src/app/models/CLAUDE.md | 49 - templates/base/src/app/snippets/CLAUDE.md | 27 - templates/base/src/app/views/CLAUDE.md | 59 - templates/base/src/config/CLAUDE.md | 842 ------ templates/base/src/plugins/CLAUDE.md | 906 ------- templates/base/src/public/CLAUDE.md | 753 ------ templates/base/src/tests/CLAUDE.md | 1362 ---------- tests/CLAUDE.md | 928 ------- 73 files changed, 2470 insertions(+), 26776 deletions(-) delete mode 100755 .ai/CLAUDE.md delete mode 100644 .ai/CONTRIBUTION_SUMMARY.md delete mode 100755 .ai/MCP-ENFORCEMENT.md delete mode 100644 .ai/QUICK_REFERENCE.md delete mode 100755 .ai/wheels/workflows/documentation-loading-strategy.md delete mode 100755 .ai/wheels/workflows/enhanced-mcp-develop-specification.md delete mode 100755 .ai/wheels/workflows/intelligent-analysis-planning-engine.md delete mode 100755 .ai/wheels/workflows/pre-implementation.md delete mode 100755 .ai/wheels/workflows/template-driven-implementation-patterns.md create mode 100644 .claude/commands/wheels_build.md delete mode 100755 .claude/commands/wheels_execute.md create mode 100644 .claude/commands/wheels_spec.md create mode 100644 .claude/commands/wheels_validate.md delete mode 100755 .claude/skills/README.md delete mode 100755 .claude/skills/SKILLS-QUICK-START.md delete mode 100755 .claude/skills/wheels-anti-pattern-detector/SKILL.md delete mode 100755 .claude/skills/wheels-api-generator/SKILL.md delete mode 100755 .claude/skills/wheels-auth-generator/SKILL.md create mode 100644 .claude/skills/wheels-codegen/SKILL.md rename .claude/skills/{wheels-model-generator => wheels-codegen}/templates/basic-model.cfc (100%) rename .claude/skills/{wheels-model-generator => wheels-codegen}/templates/user-authentication-model.cfc (100%) delete mode 100755 .claude/skills/wheels-controller-generator/SKILL.md delete mode 100755 .claude/skills/wheels-debugging/SKILL.md delete mode 100755 .claude/skills/wheels-documentation-generator/SKILL.md delete mode 100755 .claude/skills/wheels-email-generator/SKILL.md delete mode 100755 .claude/skills/wheels-migration-generator/SKILL.md delete mode 100755 .claude/skills/wheels-model-generator/SKILL.md delete mode 100755 .claude/skills/wheels-plugin-generator/SKILL.md delete mode 100755 .claude/skills/wheels-routing-generator/SKILL.md create mode 100644 .claude/skills/wheels-scaffold/SKILL.md delete mode 100755 .claude/skills/wheels-test-generator/SKILL.md create mode 100644 .claude/skills/wheels-testing/SKILL.md delete mode 100755 .claude/skills/wheels-view-generator/SKILL.md delete mode 100755 .opencode/command/wheels_execute.md delete mode 100755 examples/tweet/.ai/CLAUDE.md delete mode 100755 examples/tweet/CLAUDE.md delete mode 100755 examples/tweet/app/CLAUDE.md delete mode 100755 examples/tweet/app/controllers/CLAUDE.md delete mode 100755 examples/tweet/app/events/CLAUDE.md delete mode 100755 examples/tweet/app/global/CLAUDE.md delete mode 100755 examples/tweet/app/jobs/CLAUDE.md delete mode 100755 examples/tweet/app/lib/CLAUDE.md delete mode 100755 examples/tweet/app/mailers/CLAUDE.md delete mode 100755 examples/tweet/app/migrator/CLAUDE.md delete mode 100755 examples/tweet/app/models/CLAUDE.md delete mode 100755 examples/tweet/app/snippets/CLAUDE.md delete mode 100755 examples/tweet/app/views/CLAUDE.md delete mode 100755 examples/tweet/config/CLAUDE.md delete mode 100755 examples/tweet/plugins/CLAUDE.md delete mode 100755 examples/tweet/public/CLAUDE.md delete mode 100755 examples/tweet/tests/CLAUDE.md delete mode 100755 templates/base/src/CLAUDE.md delete mode 100755 templates/base/src/app/CLAUDE.md delete mode 100755 templates/base/src/app/controllers/CLAUDE.md delete mode 100755 templates/base/src/app/events/CLAUDE.md delete mode 100755 templates/base/src/app/global/CLAUDE.md delete mode 100755 templates/base/src/app/jobs/CLAUDE.md delete mode 100755 templates/base/src/app/lib/CLAUDE.md delete mode 100755 templates/base/src/app/mailers/CLAUDE.md delete mode 100755 templates/base/src/app/migrator/CLAUDE.md delete mode 100755 templates/base/src/app/models/CLAUDE.md delete mode 100755 templates/base/src/app/snippets/CLAUDE.md delete mode 100755 templates/base/src/app/views/CLAUDE.md delete mode 100755 templates/base/src/config/CLAUDE.md delete mode 100644 templates/base/src/plugins/CLAUDE.md delete mode 100644 templates/base/src/public/CLAUDE.md delete mode 100755 templates/base/src/tests/CLAUDE.md delete mode 100755 tests/CLAUDE.md diff --git a/.ai/CLAUDE.md b/.ai/CLAUDE.md deleted file mode 100755 index c1a332792d..0000000000 --- a/.ai/CLAUDE.md +++ /dev/null @@ -1,179 +0,0 @@ -# Wheels Documentation Index - -🚨 **COMPREHENSIVE DOCUMENTATION INDEX** 🚨 - -This file provides the complete index of Wheels documentation for AI assistants. All technical content has been organized into the structured `.ai` folder for maximum efficiency and accuracy. - -⛔ **CRITICAL: ALWAYS READ RELEVANT DOCUMENTATION BEFORE WRITING CODE** ⛔ - -## 🚨 MANDATORY Pre-Implementation Workflow - -### 🛑 STEP 1: Critical Error Prevention (ALWAYS FIRST) -1. **`.ai/wheels/troubleshooting/common-errors.md`** - PREVENT FATAL ERRORS -2. **`.ai/wheels/patterns/validation-templates.md`** - VALIDATION CHECKLISTS - -### 📋 STEP 2: Task-Specific Documentation Loading - -#### 🏗️ For Model Development -**MANDATORY Reading Order:** -1. `.ai/wheels/models/data-handling.md` - Critical query vs array patterns -2. `.ai/wheels/models/architecture.md` - Model fundamentals and structure -3. `.ai/wheels/models/associations.md` - Relationship patterns (CRITICAL) -4. `.ai/wheels/models/validations.md` - Validation methods and patterns -5. `.ai/wheels/models/best-practices.md` - Model development guidelines - -#### 🎮 For Controller Development -**MANDATORY Reading Order:** -1. `.ai/wheels/controllers/architecture.md` - Controller fundamentals and CRUD -2. `.ai/wheels/controllers/rendering.md` - View rendering and responses -3. `.ai/wheels/controllers/filters.md` - Authentication and authorization -4. `.ai/wheels/controllers/model-interactions.md` - Controller-model patterns -5. `.ai/wheels/controllers/best-practices.md` - Controller development guidelines - -#### 📄 For View Development -**MANDATORY Reading Order:** -1. `.ai/wheels/views/data-handling.md` - CRITICAL query vs array patterns -2. `.ai/wheels/views/architecture.md` - View structure and conventions -3. `.ai/wheels/views/forms.md` - Form helpers and limitations (CRITICAL) -4. `.ai/wheels/views/layouts.md` - Layout patterns and inheritance -5. `.ai/wheels/views/best-practices.md` - View implementation checklist - -#### ⚙️ For Configuration Work -**MANDATORY Reading Order:** -1. `.ai/wheels/configuration/routing.md` - CRITICAL routing anti-patterns -2. `.ai/wheels/configuration/environments.md` - Environment settings -3. `.ai/wheels/configuration/framework-settings.md` - Global settings -4. `.ai/wheels/configuration/best-practices.md` - Configuration guidelines - -### 🔍 STEP 3: Anti-Pattern Validation (BEFORE WRITING CODE) -- [ ] ❌ **NO** mixed argument styles in Wheels functions -- [ ] ❌ **NO** ArrayLen() usage on model associations (use .recordCount) -- [ ] ❌ **NO** Rails-style nested resource routing -- [ ] ❌ **NO** emailField() or passwordField() helpers (don't exist) -- [ ] ✅ **YES** consistent arguments: ALL named OR ALL positional -- [ ] ✅ **YES** use .recordCount: `user.posts().recordCount` -- [ ] ✅ **YES** separate resource declarations -- [ ] ✅ **YES** textField() with type attribute - -## 📚 Complete Documentation Structure - -### Core Framework Components - -#### Models Documentation (`.ai/wheels/models/`) -- `architecture.md` - Model structure and fundamentals -- `data-handling.md` - Critical query vs array patterns -- `associations.md` - Relationship patterns (CRITICAL) -- `validations.md` - Validation rules and methods -- `callbacks.md` - Lifecycle hooks and events -- `methods-reference.md` - Complete method documentation -- `advanced-patterns.md` - Complex model examples -- `user-authentication.md` - Authentication model patterns -- `testing.md` - Model testing strategies -- `performance.md` - Query optimization -- `best-practices.md` - Development guidelines -- `advanced-features.md` - Timestamps and dirty tracking - -#### Controllers Documentation (`.ai/wheels/controllers/`) -- `architecture.md` - Controller structure and CRUD patterns -- `rendering.md` - View rendering, redirects, flash messages -- `filters.md` - Authentication, authorization, data loading -- `model-interactions.md` - Controller-model patterns, validation -- `api.md` - JSON/XML APIs, authentication, versioning -- `security.md` - CSRF, parameter verification, sanitization -- `testing.md` - Controller testing patterns and helpers - -#### Views Documentation (`.ai/wheels/views/`) -- `data-handling.md` - Critical query vs array patterns -- `architecture.md` - View structure and file organization -- `layouts.md` - Layout patterns and inheritance -- `partials.md` - Partial usage and patterns -- `forms.md` - Form helpers and Wheels limitations -- `helpers.md` - View helpers and custom helpers -- `advanced-patterns.md` - AJAX, performance, caching -- `testing.md` - View testing patterns -- `best-practices.md` - Implementation checklist and patterns - -#### Configuration Documentation (`.ai/wheels/configuration/`) -- `routing.md` - CRITICAL routing anti-patterns and patterns -- `environments.md` - Environment settings and switching -- `application.md` - Application.cfc settings (app.cfm) -- `framework-settings.md` - Global framework settings (settings.cfm) -- `overview.md` - File structure, loading order, general overview -- `best-practices.md` - Configuration best practices and patterns -- `troubleshooting.md` - Common issues and debugging -- `security.md` - Security considerations and hardening - -## 🚨 Critical Anti-Pattern Prevention - -### Most Common Wheels Errors -1. **Mixed Arguments**: `hasMany("comments", dependent="delete")` ❌ -2. **Query vs Array Confusion**: `ArrayLen(posts)` on query objects ❌ -3. **Rails-style Routing**: Nested resource functions ❌ -4. **Non-existent Helpers**: `emailField()`, `passwordField()` ❌ - -### Correct Patterns -1. **Consistent Arguments**: `hasMany(name="comments", dependent="delete")` ✅ -2. **Query Methods**: `posts.recordCount` ✅ -3. **Separate Resources**: `.resources("posts").resources("comments")` ✅ -4. **Wheels Helpers**: `textField(type="email")` ✅ - -## 🛠️ AI Assistant Implementation Guidelines - -### 🛑 MANDATORY Pre-Code Actions (NO EXCEPTIONS) -1. **ALWAYS** read `.ai/wheels/troubleshooting/common-errors.md` FIRST -2. **ALWAYS** read component-specific .ai documentation -3. **VALIDATE** against anti-patterns before writing any code -4. **REFERENCE** code examples from .ai documentation as templates -5. **CHECK** implementation against validation templates continuously - -### Quality Assurance Process -1. **Documentation First**: Always consult .ai documentation before coding -2. **Pattern Consistency**: Follow established patterns from .ai documentation -3. **Security Awareness**: Apply security practices from .ai documentation -4. **Convention Adherence**: Follow Wheels naming and structure conventions -5. **Validation**: Test implementations against documented standards - -## 🚀 Quick Reference Dispatchers - -### Component Quick Access -- **Models**: `app/models/CLAUDE.md` → `.ai/wheels/models/` -- **Controllers**: `app/controllers/CLAUDE.md` → `.ai/wheels/controllers/` -- **Views**: `app/views/CLAUDE.md` → `.ai/wheels/views/` -- **Configuration**: Root `CLAUDE.md` → `.ai/wheels/configuration/` - -### Critical Reading Priority -1. **Error Prevention**: `.ai/wheels/troubleshooting/common-errors.md` -2. **Data Handling**: Component-specific `data-handling.md` files -3. **Best Practices**: Component-specific `best-practices.md` files -4. **Architecture**: Component-specific `architecture.md` files - -## ✅ Post-Implementation Validation - -### MANDATORY Validation Commands -```bash -# 1. Syntax validation -wheels server start --validate - -# 2. Test validation -wheels test run - -# 3. Manual anti-pattern check -# Check implementation against .ai documentation patterns -``` - -### If Validation Fails -1. Consult `.ai/wheels/troubleshooting/common-errors.md` -2. Review appropriate component documentation in `.ai/wheels/` -3. Fix errors following documented patterns -4. Re-run validation until all checks pass - -## 🎯 Success Criteria - -**Your implementation is successful when:** -- [ ] All relevant .ai documentation has been read -- [ ] No anti-patterns are present in the code -- [ ] Patterns match those documented in .ai folder -- [ ] Validation commands pass successfully -- [ ] Code follows Wheels conventions and best practices - -🚨 **REMEMBER: The .ai folder contains the definitive, comprehensive documentation. ALWAYS use it as your primary reference!** \ No newline at end of file diff --git a/.ai/CONTRIBUTION_SUMMARY.md b/.ai/CONTRIBUTION_SUMMARY.md deleted file mode 100644 index 72cd67c3a3..0000000000 --- a/.ai/CONTRIBUTION_SUMMARY.md +++ /dev/null @@ -1,162 +0,0 @@ -# .ai Folder Contribution Summary - -## Overview - -This `.ai` folder contains comprehensive Wheels framework documentation extracted from real-world development sessions. The patterns, best practices, and solutions documented here are production-tested and ready for contribution to the Wheels project. - -## Generic Patterns Integrated (Ready for Wheels Project) - -### 🔴 Critical Patterns (High Impact for All Developers) - -#### 1. **Layout cfoutput Block Coverage** → [views/layouts.md](wheels/views/layouts.md) -- **Issue**: Most common beginner error - CFML expressions not rendering -- **Solution**: Single `` block wrapping entire HTML layout -- **Impact**: Affects 90%+ of new Wheels developers -- **Location**: Enhanced in `common-errors.md` and `views/layouts.md` - -#### 2. **Form Helper Duplicate Labels** → [views/forms.md](wheels/views/forms.md) -- **Issue**: Form helpers automatically generate labels, causing duplicates -- **Solution**: Use `label=false` parameter when using custom HTML labels -- **Impact**: Very common UX issue that confuses developers -- **Location**: Documented in `views/forms.md` and `common-errors.md` - -#### 3. **Query vs Object Association Access** → [views/query-association-patterns.md](wheels/views/query-association-patterns.md) & [models/associations.md](wheels/models/associations.md) -- **Issue**: Confusion between query objects and model instances in loops -- **Solution**: Store association results in variables before looping -- **Impact**: Fundamental misunderstanding causing frequent errors -- **Location**: Comprehensive guide in `query-association-patterns.md` - -#### 4. **Consistent Argument Style Requirement** → [common-errors.md](wheels/troubleshooting/common-errors.md) -- **Issue**: Mixed positional/named arguments cause cryptic errors -- **Solution**: Always use all-named or all-positional arguments -- **Impact**: Frequent error that's not obvious to fix -- **Location**: Examples throughout all documentation files - -### 🗄️ Database & Migration Patterns - -#### 5. **Database-Agnostic Date Handling** → [database/migrations/date-function-issues.md](wheels/database/migrations/date-function-issues.md) -- **Pattern**: Use CFML DateAdd/DateFormat instead of database-specific SQL -- **Benefit**: Works across H2, MySQL, PostgreSQL, SQL Server -- **Impact**: Portable migrations for all environments -- **Location**: Comprehensive guide with examples - -#### 6. **Migration Direct SQL Best Practice** → [database/migrations/best-practices.md](wheels/database/migrations/best-practices.md) -- **Pattern**: Direct SQL more reliable than parameter binding for data seeding -- **Benefit**: Consistent migration execution across environments -- **Impact**: Reduces migration failures -- **Location**: New comprehensive best practices guide - -#### 7. **Working with Existing Schemas** → [database/migrations/best-practices.md](wheels/database/migrations/best-practices.md) -- **Pattern**: Check for existing tables before creating migrations -- **Solution**: Use `changeTable()` for additions to existing schema -- **Impact**: Prevents "table already exists" errors -- **Location**: New section in best-practices.md - -### 🧪 Testing Strategies - -#### 8. **Content Verification Over Status Codes** → [views/testing.md](wheels/views/testing.md) -- **Issue**: HTTP 200 doesn't mean content rendered correctly -- **Solution**: Verify actual content with grep/pattern matching -- **Impact**: Catches rendering errors that status codes miss -- **Location**: New critical section in testing.md - -#### 9. **Incremental Testing Approach** → [views/testing.md](wheels/views/testing.md) -- **Pattern**: Test after each component (model → controller → view) -- **Benefit**: Isolates issues quickly, prevents compound errors -- **Impact**: Faster debugging and validation -- **Location**: Command-line testing strategy in testing.md - -### 🎨 Modern Frontend Integration - -#### 10. **CDN-Based Frontend Stack** → [integration/modern-frontend-stack.md](wheels/integration/modern-frontend-stack.md) -- **Pattern**: Tailwind CSS + Alpine.js + HTMX via CDN -- **Benefit**: No build process, works seamlessly with CFML -- **Impact**: Modern UI without complexity -- **Location**: Complete integration guide with production examples - -### 🛣️ Routing & Configuration - -#### 11. **Route Configuration Order** → [configuration/routing.md](wheels/configuration/routing.md) -- **Pattern**: Resources → Custom → Root → Wildcard -- **Impact**: Prevents route matching conflicts -- **Location**: Routing guide with examples - -## Files Modified/Created - -### Enhanced Existing Files: -- ✅ `wheels/troubleshooting/common-errors.md` - Added layout cfoutput errors -- ✅ `wheels/views/layouts.md` - Enhanced with cfoutput block rules -- ✅ `wheels/views/forms.md` - Already had duplicate label warnings -- ✅ `wheels/views/query-association-patterns.md` - Already comprehensive -- ✅ `wheels/models/associations.md` - Already had query return documentation -- ✅ `wheels/database/migrations/date-function-issues.md` - Added database-agnostic section -- ✅ `wheels/views/testing.md` - Added content verification section -- ✅ `wheels/configuration/routing.md` - Already had ordering guidance -- ✅ `wheels/integration/modern-frontend-stack.md` - Added key findings summary - -### New Files Created: -- ✨ `wheels/database/migrations/best-practices.md` - Comprehensive migration guide including existing schema handling - -### Session Files Removed: -- ❌ `wheels/troubleshooting/session-learnings-2024-09-17.md` - Patterns integrated -- ❌ `wheels/troubleshooting/session-learnings-2025-10-01.md` - Patterns integrated - -## Value Proposition for Wheels Project - -### Documentation Improvements: -1. **Common Mistakes Guide** - Layout cfoutput, duplicate labels, query vs object -2. **Migration Best Practices** - Data seeding, database-agnostic SQL, existing schemas -3. **Modern Frontend Integration** - Tailwind/Alpine.js/HTMX patterns -4. **Testing Strategies** - Content verification over status codes - -### Framework Enhancement Opportunities: -1. ⚠️ **Warning System** - Detect mixed positional/named arguments at runtime -2. ⚠️ **Generator Improvements** - Better handling of existing database schemas -3. ⚠️ **Form Helper Defaults** - Clearer documentation of label generation behavior - -### Example Code/Scaffolding: -1. 📦 **Modern Blog Starter Template** - Demonstrating all patterns -2. 📦 **Layout Templates** - Proper cfoutput block structure -3. 📦 **Migration Examples** - Database-agnostic patterns - -## Usage - -This documentation is **project-agnostic** and represents patterns discovered through multiple Wheels implementations. Each pattern: - -- ✅ Has been tested in real applications -- ✅ Solves documented pain points -- ✅ Includes clear examples and anti-patterns -- ✅ Benefits the entire Wheels community - -## Contribution Path - -### For Wheels Core Team: - -**Immediate Documentation Additions:** -1. Add to official Wheels documentation site -2. Create "Common Mistakes" guide -3. Enhance migration documentation with best practices -4. Add modern frontend integration examples - -**Framework Enhancements (Optional):** -1. Add warning for mixed argument styles -2. Improve generator schema detection -3. Enhance form helper documentation - -### For Wheels Community: - -**Share as Community Resources:** -1. Wheels GitHub Discussions -2. Wheels Slack/Discord channels -3. Blog posts and tutorials -4. Conference presentations - -## License & Attribution - -These patterns are contributed to the Wheels project under the same license as Wheels (Apache 2.0). They represent collective knowledge from production implementations and are freely available for the community. - ---- - -**Generated**: 2025-10-02 -**Source**: Multiple production Wheels applications -**Status**: Ready for contribution to Wheels project diff --git a/.ai/MCP-ENFORCEMENT.md b/.ai/MCP-ENFORCEMENT.md deleted file mode 100755 index e05af590e7..0000000000 --- a/.ai/MCP-ENFORCEMENT.md +++ /dev/null @@ -1,70 +0,0 @@ -# 🚨 MCP Enforcement Guidelines - -## Mandatory MCP Usage Check - -**AI assistants MUST perform this check before ANY development work:** - -```bash -# Check if MCP tools are mandatory -ls .mcp.json -``` - -**If `.mcp.json` exists → MCP tools are MANDATORY** - -## 🚫 FORBIDDEN Commands (when .mcp.json exists) - -```bash -# NEVER use these when MCP is available: -wheels g model -wheels g controller -wheels g migration -wheels g scaffold -wheels dbmigrate -wheels test -wheels server -curl http://localhost:*/?reload=true -``` - -## ✅ MANDATORY Commands (when .mcp.json exists) - -```javascript -// ALWAYS use these instead: -mcp__wheels__wheels_generate(type="model", ...) -mcp__wheels__wheels_generate(type="controller", ...) -mcp__wheels__wheels_generate(type="migration", ...) -mcp__wheels__wheels_generate(type="scaffold", ...) -mcp__wheels__wheels_migrate(action="...") -mcp__wheels__wheels_test() -mcp__wheels__wheels_server(action="...") -mcp__wheels__wheels_reload() -``` - -## Enforcement Rules - -1. **Check .mcp.json first** - always -2. **Use MCP tools exclusively** - if available -3. **Never mix CLI and MCP** - pick one based on availability -4. **Test MCP connection** - before starting work -5. **Follow MCP patterns** - as documented in CLAUDE.md - -## Violation Detection - -**If you catch yourself or another AI assistant doing ANY of the following:** -- Using `wheels g` commands when `.mcp.json` exists -- Using `wheels dbmigrate` when MCP is available -- Using `curl` for reload when `mcp__wheels__wheels_reload()` is available -- Mixing CLI and MCP approaches - -**→ STOP immediately and switch to MCP tools** - -## Benefits of MCP Tools - -1. **Better Integration** - Direct integration with Wheels application -2. **Error Handling** - Improved error reporting and handling -3. **Consistency** - Standardized interface across all operations -4. **Validation** - Built-in validation and safety checks -5. **Documentation** - Access to real-time project documentation - ---- - -**Remember: MCP tools provide a superior development experience when available. Always use them when `.mcp.json` exists.** \ No newline at end of file diff --git a/.ai/QUICK_REFERENCE.md b/.ai/QUICK_REFERENCE.md deleted file mode 100644 index d1c6364a65..0000000000 --- a/.ai/QUICK_REFERENCE.md +++ /dev/null @@ -1,244 +0,0 @@ -# Wheels Quick Reference - Most Common Issues - -## 🔴 Top 5 Critical Patterns (Learn These First!) - -### 1. Layout cfoutput Block Coverage (MOST COMMON ERROR) - -❌ **WRONG - Expressions don't render:** -```cfm - - #includeContent()# - - - - - #csrfMetaTags()# - - - Posts - - - -``` - -✅ **CORRECT - Wrap entire HTML in cfoutput:** -```cfm - - #includeContent()# - - - - - - #csrfMetaTags()# - - - Posts - #includeContent()# - - - - -``` - -**Rule**: Open `` after ``, close before `` - ---- - -### 2. Form Helper Duplicate Labels - -❌ **WRONG - Creates duplicate labels:** -```cfm - -#textField(objectName="post", property="title")# -``` -**Result**: "Title Title" appears - -✅ **CORRECT - Use label=false:** -```cfm - -#textField(objectName="post", property="title", label=false)# -``` - -**Rule**: When using HTML labels, add `label=false` to form helpers - ---- - -### 3. Association Query Handling in Views - -❌ **WRONG - Associations aren't counted properties:** -```cfm - -

#posts.comments_count# comments

-
-``` - -✅ **CORRECT - Load association and use recordCount:** -```cfm - - -

#comments.recordCount# comments

-
-``` - -**Rule**: Associations return QUERY objects with `.recordCount`, not computed properties - ---- - -### 4. Consistent Argument Style - -❌ **WRONG - Mixed positional and named:** -```cfm -hasMany("comments", dependent="delete") -model("Post").findByKey(1, include="comments") -``` - -✅ **CORRECT - All named arguments:** -```cfm -hasMany(name="comments", dependent="delete") -model("Post").findByKey(key=1, include="comments") -``` - -**Rule**: Never mix positional and named arguments - ---- - -### 5. Database-Agnostic Migration Dates - -❌ **WRONG - Database-specific functions:** -```cfm -execute("INSERT INTO posts (publishedAt) VALUES (DATE_SUB(NOW(), INTERVAL 7 DAY))"); -``` -**Problem**: Only works with MySQL - -✅ **CORRECT - Use CFML date functions:** -```cfm -var day7 = DateAdd("d", -7, Now()); -execute("INSERT INTO posts (publishedAt) VALUES ( - TIMESTAMP '#DateFormat(day7, "yyyy-mm-dd")# #TimeFormat(day7, "HH:mm:ss")#' -)"); -``` - -**Rule**: Use CFML DateAdd/DateFormat for portability - ---- - -## 🧪 Testing Best Practice - -❌ **WRONG - Only check status code:** -```bash -curl -I http://localhost:8080 | grep "200 OK" -``` -**Problem**: 200 doesn't mean content is correct - -✅ **CORRECT - Verify actual content:** -```bash -curl -s http://localhost:8080 | grep "Expected Content" -curl -s http://localhost:8080 | grep -c "article" # Count elements -curl -s http://localhost:8080 | grep '#urlFor' # Should be empty -``` - -**Rule**: Always verify content rendering, not just HTTP status - ---- - -## 🗄️ Migration Best Practices - -### Check Before Creating Tables - -❌ **WRONG - Assume clean database:** -```cfm -function up() { - t = createTable(name="posts"); - t.create(); -} -``` -**Error**: "Table already exists" - -✅ **CORRECT - Check existing schema first:** -```bash -# Before creating migration -wheels dbmigrate info # Check current state - -# If table exists, modify instead -t = changeTable(name="posts"); -t.text(columnNames="excerpt"); # Add missing column only -t.change(); -``` - -### Direct SQL for Data Seeding - -❌ **WRONG - Parameter binding:** -```cfm -execute(sql="INSERT INTO posts VALUES (?)", parameters=[{value=title}]); -``` - -✅ **CORRECT - Direct SQL:** -```cfm -execute("INSERT INTO posts (title, createdAt, updatedAt) - VALUES ('My Post', NOW(), NOW())"); -``` - ---- - -## 🛣️ Route Configuration Order - -❌ **WRONG - Wildcard first:** -```cfm -mapper() - .wildcard() - .resources("posts") -.end(); -``` - -✅ **CORRECT - Specific to general:** -```cfm -mapper() - .resources("posts") // 1. Resource routes - .get(name="about", ...) // 2. Custom routes - .root(to="posts##index") // 3. Root route - .wildcard() // 4. Wildcard LAST -.end(); -``` - ---- - -## 🔍 Quick Debugging Checklist - -When something doesn't work, check in this order: - -### Views Not Rendering: -1. ✅ Is entire layout in `` block? -2. ✅ Did you reload after changes (`?reload=true`)? -3. ✅ Are associations stored in variables before loops? - -### Forms Have Issues: -1. ✅ Did you add `label=false` when using custom labels? -2. ✅ Are arguments all named or all positional (not mixed)? -3. ✅ Did you test delete buttons with `method="delete"`? - -### Migrations Failing: -1. ✅ Did you check if tables already exist? -2. ✅ Are you using CFML date functions, not database-specific? -3. ✅ Are operations wrapped in `transaction` blocks? - -### Routes Not Working: -1. ✅ Is route order correct (resources → custom → root → wildcard)? -2. ✅ Did mapper end with `.end()`? -3. ✅ Did you reload routes (`?reload=true`)? - ---- - -## 📚 Full Documentation References - -- **Layout Issues**: [views/layouts.md](wheels/views/layouts.md) -- **Form Problems**: [views/forms.md](wheels/views/forms.md) -- **Query/Association**: [views/query-association-patterns.md](wheels/views/query-association-patterns.md) -- **All Common Errors**: [troubleshooting/common-errors.md](wheels/troubleshooting/common-errors.md) -- **Migration Best Practices**: [database/migrations/best-practices.md](wheels/database/migrations/best-practices.md) -- **Testing Strategies**: [views/testing.md](wheels/views/testing.md) -- **Modern Frontend**: [integration/modern-frontend-stack.md](wheels/integration/modern-frontend-stack.md) - ---- - -**Remember**: These 5 patterns solve 80%+ of common Wheels issues. Master them first! diff --git a/.ai/README.md b/.ai/README.md index 2cbbae7ea3..8fba0d2f69 100755 --- a/.ai/README.md +++ b/.ai/README.md @@ -1,142 +1,31 @@ -# Wheels Framework AI Knowledge Base - -## Overview -This knowledge base contains comprehensive information about CFML (ColdFusion Markup Language) and the Wheels framework, organized for AI coding assistants. The documentation is structured in two main sections to provide both foundational language knowledge and framework-specific guidance. - -## Structure - -The documentation is organized into two main sections: - -### 📁 [CFML Language Documentation](./cfml/) -Core CFML language concepts, syntax, and features that apply to all CFML frameworks: - -- **[Syntax](./cfml/syntax/)** - Basic CFML syntax, CFScript vs tags, comments -- **[Data Types](./cfml/data-types/)** - Variables, arrays, strings, structures, numbers, scopes -- **[Control Flow](./cfml/control-flow/)** - Conditionals, loops, exception handling -- **[Components](./cfml/components/)** - CFC basics, functions, properties -- **[Database](./cfml/database/)** - Query fundamentals and database interaction -- **[Advanced](./cfml/advanced/)** - Closures and advanced language features -- **[Best Practices](./cfml/best-practices/)** - Modern CFML development patterns - -### 📁 [Wheels Framework Documentation](./wheels/) -Framework-specific patterns, conventions, and features: - -#### Core Framework Areas -- **[CLI Tools](./wheels/cli/)** - Generators, server management, testing tools -- **[Configuration](./wheels/configuration/)** - Environment settings, framework configuration -- **[Controllers](./wheels/controllers/)** - Request handling, filters, rendering, parameters -- **[Core Concepts](./wheels/core-concepts/)** - MVC architecture, ORM, routing conventions -- **[Database](./wheels/database/)** - ActiveRecord ORM, migrations, associations, validations -- **[Views](./wheels/views/)** - Templates, layouts, helpers, assets - -#### Development Support -- **[Communication](./wheels/communication/)** - Email, HTTP requests, API development -- **[Files](./wheels/files/)** - File uploads, downloads, asset management -- **[Patterns](./wheels/patterns/)** - Common development patterns and best practices -- **[Security](./wheels/security/)** - Authentication, authorization, CSRF protection -- **[Snippets](./wheels/snippets/)** - Code examples and quick reference patterns - -## Quick Reference - -### Getting Started with CFML + Wheels -1. **Understand CFML Basics**: Start with [CFML syntax](./cfml/syntax/) and [data types](./cfml/data-types/) -2. **Learn Wheels Conventions**: Review [MVC architecture](./wheels/core-concepts/) patterns -3. **Use CLI Tools**: Leverage [generators](./wheels/cli/) for rapid development - -### 🚨 CRITICAL: MCP Tools Required -**If `.mcp.json` exists, use MCP tools exclusively - CLI commands are FORBIDDEN** - -### ✅ MCP Development Tasks (MANDATORY when .mcp.json exists) -```javascript -// Generate application components -mcp__wheels__wheels_generate(type="model", name="User", attributes="name:string,email:string,active:boolean") -mcp__wheels__wheels_generate(type="controller", name="Users", actions="index,show,new,create,edit,update,delete") -mcp__wheels__wheels_generate(type="migration", name="CreateUsersTable") - -// Database operations -mcp__wheels__wheels_migrate(action="latest") -mcp__wheels__wheels_migrate(action="up") -mcp__wheels__wheels_migrate(action="down") - -// Server management -mcp__wheels__wheels_server(action="start") -mcp__wheels__wheels_server(action="stop") -mcp__wheels__wheels_test() -mcp__wheels__wheels_reload() -``` - -### ❌ Legacy CLI Commands (DO NOT USE if .mcp.json exists) -```bash -# These are FORBIDDEN when MCP tools are available -wheels g model User name:string,email:string,active:boolean -wheels g controller Users index,show,new,create,edit,update,delete -wheels dbmigrate latest -wheels server start -wheels test run -``` - -### Key Framework Concepts -- **Models are singular**: User.cfc → users table -- **Controllers are plural**: Users.cfc handles users resource -- **Routes use resources**: `resources("users")` creates RESTful routes -- **Validations in models**: `validatesPresenceOf("name,email")` -- **Associations**: `hasMany("orders")`, `belongsTo("user")` -- **CFScript preferred**: Modern CFML uses CFScript over tag-based syntax - -## Documentation Structure -Each documentation file follows a consistent structure: -- **Description**: Brief explanation of the concept -- **Key Points**: Important facts and features -- **Code Sample**: Practical, working examples -- **Usage**: Step-by-step instructions -- **Related**: Links to connected concepts -- **Important Notes**: Common pitfalls and best practices - -## How to Use This Knowledge Base - -### For AI Assistants -1. **Start with context**: Determine if the question is about CFML language fundamentals or Wheels framework specifics -2. **Reference appropriately**: Use CFML docs for language questions, Wheels docs for framework patterns -3. **Combine knowledge**: Many questions require understanding both CFML syntax and Wheels conventions -4. **Follow patterns**: Use the established code examples and patterns shown in the documentation - -### For Developers -1. **New to CFML?** Start with [CFML fundamentals](./cfml/) -2. **New to Wheels?** Begin with [core concepts](./wheels/core-concepts/) -3. **Specific task?** Check [snippets](./wheels/snippets/) and [patterns](./wheels/patterns/) -4. **Configuration issues?** Review [configuration](./wheels/configuration/) docs - -## Best Practices - -### CFML Development -- Use CFScript syntax for modern, readable code -- Understand variable scoping and lifecycle -- Leverage CFML's dynamic nature while maintaining type safety -- Follow contemporary CFML patterns from the best practices section - -### Wheels Framework -1. **Follow Conventions**: Wheels rewards convention over configuration -2. **Keep Controllers Thin**: Business logic belongs in models -3. **Use Validations**: Validate data at the model level -4. **Secure by Default**: Use CSRF protection and parameter verification -5. **Test Everything**: Write tests for models, controllers, and integrations -6. **Leverage the ORM**: Use Wheels' ActiveRecord patterns for database operations - -## Integration with Development Tools - -This knowledge base is designed to work with: -- **AI coding assistants** (Claude Code, GitHub Copilot, etc.) -- **MCP (Model Context Protocol)** clients -- **Wheels development server** (AI documentation endpoints) -- **IDE integrations** and development workflows - -## Contributing -This knowledge base combines: -- Official Wheels framework documentation -- CFML language reference materials -- Modern CFML development practices -- Community best practices and patterns - ---- - -*Organized for comprehensive CFML and Wheels framework development support* \ No newline at end of file +# Wheels Framework Reference Docs + +Searchable reference for CFML language and Wheels framework patterns. + +## CFML Language (`cfml/`) + +- `syntax/` — CFScript basics, tags vs script, comments, hash escaping +- `data-types/` — Variables, scopes, arrays, strings, structures, numbers +- `control-flow/` — Conditionals, loops, exception handling +- `components/` — CFC basics, functions, properties +- `database/` — Query fundamentals +- `advanced/` — Closures, advanced features +- `best-practices/` — Modern CFML patterns + +## Wheels Framework (`wheels/`) + +- `models/` — ORM architecture, associations, validations, performance +- `controllers/` — Actions, filters, rendering, security, parameters +- `views/` — Layouts, partials, form helpers, link helpers, assets +- `database/` — Migrations, associations, queries, validations +- `configuration/` — Routing, environments, settings +- `core-concepts/` — MVC architecture, ORM mapping, routing conventions +- `cli/` — Generators, server management +- `communication/` — Email sending +- `files/` — File uploads and downloads +- `security/` — CSRF, authentication, authorization +- `patterns/` — Common development patterns, validation templates +- `snippets/` — Code examples for all component types +- `integration/` — Frontend stack (Tailwind, Alpine, HTMX) +- `testing/` — Testing strategies and patterns +- `troubleshooting/` — Common errors and debugging diff --git a/.ai/wheels/workflows/documentation-loading-strategy.md b/.ai/wheels/workflows/documentation-loading-strategy.md deleted file mode 100755 index d6ce5edec7..0000000000 --- a/.ai/wheels/workflows/documentation-loading-strategy.md +++ /dev/null @@ -1,510 +0,0 @@ -# Systematic Documentation Loading Strategy - -## Overview - -This strategy defines how the enhanced `mcp__wheels__develop` workflow automatically loads relevant documentation from the `.ai` folder based on task analysis. The goal is to ensure AI assistants always have the correct patterns, anti-patterns, and best practices loaded before beginning any implementation work. - -## Core Principles - -1. **Context-Aware Loading**: Load documentation based on task type detection -2. **Priority-Based Access**: Critical error prevention documentation always loads first -3. **Progressive Enhancement**: Load basic patterns first, then advanced features -4. **Consistency Enforcement**: Ensure same patterns are applied across similar components -5. **Error Prevention**: Always prioritize anti-pattern documentation - -## Documentation Loading Architecture - -### Phase 1: Universal Critical Documentation (ALWAYS FIRST) - -**Mandatory files loaded for EVERY task:** -``` -1. .ai/wheels/troubleshooting/common-errors.md [CRITICAL - Error Prevention] -2. .ai/wheels/patterns/validation-templates.md [CRITICAL - Anti-Pattern Checklists] -3. .ai/wheels/workflows/pre-implementation.md [WORKFLOW - Process Validation] -``` - -**Purpose**: Prevent the two most common Wheels errors: -- Mixed argument styles in function calls -- Query/Array confusion in loops and counts - -### Phase 2: Task Type Detection Engine - -#### Natural Language Analysis Patterns -``` -Task Analysis Keywords → Component Type Detection: - -Model Indicators: -- "model", "User", "Post", "Comment", "Product", "Order" -- "association", "hasMany", "belongsTo", "validation" -- "database", "table", "record", "ActiveRecord" -- "save", "create", "update", "delete", "find" - -Controller Indicators: -- "controller", "action", "CRUD", "REST", "API" -- "filter", "authentication", "authorization" -- "render", "redirect", "session", "flash" -- "index", "show", "new", "create", "edit", "update", "destroy" - -View Indicators: -- "view", "template", "form", "layout", "partial" -- "helper", "display", "render", "output" -- "HTML", "CSS", "JavaScript", "Alpine.js", "HTMX" -- "responsive", "mobile", "desktop" - -Migration Indicators: -- "migration", "migrate", "schema", "column", "table" -- "database", "create table", "alter table", "index" -- "rollback", "up", "down", "version" - -Configuration Indicators: -- "route", "routing", "URL", "endpoint" -- "config", "settings", "environment" -- "security", "CSRF", "authentication" -``` - -#### Task Complexity Assessment -``` -Simple Task (1-2 components): -- "create a User model" -- "add validation to Post" -- "create contact form" - -Moderate Task (3-5 components): -- "create blog with posts and comments" -- "add user authentication" -- "create product catalog" - -Complex Task (6+ components): -- "create e-commerce site with users, products, orders, payments" -- "build admin dashboard with user management" -- "create multi-tenant blog platform" -``` - -### Phase 3: Component-Specific Documentation Loading - -#### Model Development Documentation Stack -``` -Primary Load Order: -1. .ai/wheels/models/architecture.md [Foundation patterns] -2. .ai/wheels/models/associations.md [Relationship handling] -3. .ai/wheels/models/validations.md [Validation patterns] -4. .ai/wheels/models/best-practices.md [Development guidelines] -5. .ai/wheels/snippets/model-snippets.md [Code templates] - -Secondary Load (Task-Specific): -IF associations detected: - → .ai/wheels/database/associations/has-many.md - → .ai/wheels/database/associations/belongs-to.md - → .ai/wheels/database/associations/has-one.md - -IF validation requirements: - → .ai/wheels/database/validations/presence.md - → .ai/wheels/database/validations/uniqueness.md - → .ai/wheels/database/validations/format.md - → .ai/wheels/database/validations/custom.md - -IF authentication model: - → .ai/wheels/models/user-authentication.md - → .ai/wheels/security/csrf-protection.md - -IF performance concerns: - → .ai/wheels/models/performance.md - → .ai/wheels/database/queries/finding-records.md -``` - -#### Controller Development Documentation Stack -``` -Primary Load Order: -1. .ai/wheels/controllers/architecture.md [Foundation patterns] -2. .ai/wheels/controllers/rendering.md [View handling] -3. .ai/wheels/controllers/model-interactions.md [Data layer patterns] -4. .ai/wheels/controllers/filters.md [Request processing] -5. .ai/wheels/snippets/controller-snippets.md [Code templates] - -Secondary Load (Task-Specific): -IF authentication required: - → .ai/wheels/controllers/security.md - → .ai/wheels/patterns/authentication.md - -IF API development: - → .ai/wheels/controllers/api.md - → .ai/wheels/controllers/http-detection.md - -IF admin functionality: - → .ai/wheels/controllers/filters.md (authorization) - → .ai/wheels/controllers/security.md - -IF CRUD operations: - → .ai/wheels/patterns/crud.md - → .ai/wheels/controllers/model-interactions.md -``` - -#### View Development Documentation Stack -``` -Primary Load Order: -1. .ai/wheels/views/data-handling.md [CRITICAL - Query patterns] -2. .ai/wheels/views/architecture.md [Structure patterns] -3. .ai/wheels/views/forms.md [Form helper patterns] -4. .ai/wheels/views/layouts.md [Layout patterns] -5. .ai/wheels/snippets/view-snippets.md [Code templates] - -Secondary Load (Task-Specific): -IF forms detected: - → .ai/wheels/views/forms.md - → .ai/wheels/views/helpers/forms.md - → .ai/wheels/security/csrf-protection.md - -IF layout work: - → .ai/wheels/views/layouts/structure.md - → .ai/wheels/views/layouts/partials.md - → .ai/wheels/views/layouts/content-for.md - -IF helpers needed: - → .ai/wheels/views/helpers/links.md - → .ai/wheels/views/helpers/dates.md - → .ai/wheels/views/helpers/custom.md - -IF responsive design: - → .ai/wheels/views/advanced-patterns.md - → .ai/cfml/syntax/cfscript-vs-tags.md -``` - -#### Migration Development Documentation Stack -``` -Primary Load Order: -1. .ai/wheels/database/migrations/creating-migrations.md -2. .ai/wheels/database/migrations/column-types.md -3. .ai/wheels/database/migrations/advanced-operations.md -4. .ai/wheels/database/migrations/running-migrations.md - -Secondary Load (Task-Specific): -IF complex schema changes: - → .ai/wheels/database/migrations/rollback.md - -IF data seeding required: - → .ai/wheels/troubleshooting/common-errors.md (parameter binding issues) - -IF indexes needed: - → .ai/wheels/database/migrations/advanced-operations.md -``` - -### Phase 4: Cross-Component Documentation Loading - -#### Multi-Component Project Documentation -``` -For projects requiring multiple components: - -1. Load all primary stacks for detected components -2. Load integration documentation: - → .ai/wheels/core-concepts/routing/patterns.md - → .ai/wheels/configuration/routing.md - → .ai/wheels/patterns/crud.md - -3. Load security documentation: - → .ai/wheels/security/csrf-protection.md - → .ai/wheels/controllers/security.md - -4. Load testing documentation: - → .ai/wheels/models/testing.md - → .ai/wheels/controllers/testing.md - → .ai/wheels/views/testing.md -``` - -### Phase 5: CFML Language Documentation Loading - -#### Syntax-Specific Documentation -``` -Load based on syntax patterns needed: - -IF CFScript detected/preferred: - → .ai/cfml/syntax/cfscript-vs-tags.md - → .ai/cfml/syntax/basic-syntax.md - → .ai/cfml/components/component-basics.md - -IF complex data handling: - → .ai/cfml/data-types/variables.md - → .ai/cfml/data-types/variable-scopes.md - → .ai/cfml/control-flow/loops.md - -IF query operations: - → .ai/cfml/database/query-basics.md - → .ai/cfml/control-flow/loops.md - -IF error handling needed: - → .ai/cfml/control-flow/exception-handling.md - → .ai/cfml/control-flow/conditionals.md -``` - -## Documentation Loading Implementation - -### Smart Loading Algorithm -```javascript -function loadDocumentationForTask(taskDescription, taskComplexity) { - const documentationQueue = []; - - // Phase 1: Always load critical documentation first - documentationQueue.push(...CRITICAL_DOCUMENTATION); - - // Phase 2: Analyze task and determine component types - const components = analyzeTaskComponents(taskDescription); - - // Phase 3: Load component-specific documentation - for (const component of components) { - documentationQueue.push(...getComponentDocumentation(component)); - } - - // Phase 4: Load cross-component documentation if needed - if (components.length > 1) { - documentationQueue.push(...INTEGRATION_DOCUMENTATION); - } - - // Phase 5: Load CFML documentation as needed - const syntaxNeeds = analyzeSyntaxRequirements(taskDescription, components); - documentationQueue.push(...getCFMLDocumentation(syntaxNeeds)); - - // Phase 6: Load advanced features based on complexity - if (taskComplexity === 'complex') { - documentationQueue.push(...ADVANCED_DOCUMENTATION); - } - - return documentationQueue; -} -``` - -### Component Detection Patterns -```javascript -function analyzeTaskComponents(taskDescription) { - const components = []; - const text = taskDescription.toLowerCase(); - - // Model detection - if (hasModelIndicators(text)) { - components.push('model'); - - // Specific model features - if (text.includes('association') || text.includes('relationship')) { - components.push('model_associations'); - } - if (text.includes('validation') || text.includes('validate')) { - components.push('model_validations'); - } - if (text.includes('user') || text.includes('auth')) { - components.push('model_authentication'); - } - } - - // Controller detection - if (hasControllerIndicators(text)) { - components.push('controller'); - - // Specific controller features - if (text.includes('api') || text.includes('json')) { - components.push('controller_api'); - } - if (text.includes('admin') || text.includes('auth')) { - components.push('controller_security'); - } - if (text.includes('crud') || text.includes('rest')) { - components.push('controller_crud'); - } - } - - // View detection - if (hasViewIndicators(text)) { - components.push('view'); - - // Specific view features - if (text.includes('form') || text.includes('input')) { - components.push('view_forms'); - } - if (text.includes('layout') || text.includes('template')) { - components.push('view_layouts'); - } - if (text.includes('responsive') || text.includes('mobile')) { - components.push('view_responsive'); - } - } - - // Migration detection - if (hasMigrationIndicators(text)) { - components.push('migration'); - - if (text.includes('seed') || text.includes('data')) { - components.push('migration_seeding'); - } - } - - return components; -} -``` - -### Documentation Priority Matrix -``` -Priority 1 (Critical - Always Load): -- common-errors.md -- validation-templates.md -- pre-implementation.md - -Priority 2 (Foundation - Load for Component Type): -- architecture.md files -- basic patterns - -Priority 3 (Feature-Specific - Load as Needed): -- associations.md -- validations.md -- forms.md -- security.md - -Priority 4 (Advanced - Load for Complex Tasks): -- performance.md -- advanced-patterns.md -- testing.md - -Priority 5 (Reference - Load on Demand): -- methods-reference.md -- snippets.md -- troubleshooting files -``` - -### Documentation Validation -```javascript -function validateDocumentationLoaded(components, taskComplexity) { - const requiredDocs = []; - - // Always required - requiredDocs.push('common-errors.md', 'validation-templates.md'); - - // Component-specific requirements - if (components.includes('model')) { - requiredDocs.push('models/architecture.md', 'models/best-practices.md'); - } - if (components.includes('controller')) { - requiredDocs.push('controllers/architecture.md', 'controllers/rendering.md'); - } - if (components.includes('view')) { - requiredDocs.push('views/data-handling.md', 'views/architecture.md'); - } - - // Verify all required documentation is loaded - for (const doc of requiredDocs) { - if (!isDocumentationLoaded(doc)) { - throw new Error(`Required documentation not loaded: ${doc}`); - } - } - - return true; -} -``` - -## Documentation Context Management - -### Context Preservation -```javascript -function preserveDocumentationContext(loadedDocs) { - // Store loaded documentation in context for reference during implementation - const context = { - antiPatterns: extractAntiPatterns(loadedDocs), - codeTemplates: extractTemplates(loadedDocs), - bestPractices: extractBestPractices(loadedDocs), - securityRules: extractSecurityRules(loadedDocs), - argumentStyles: detectArgumentStyles(loadedDocs) - }; - - return context; -} -``` - -### Pattern Extraction -```javascript -function extractAntiPatterns(documentation) { - // Extract ❌ marked patterns from documentation - const antiPatterns = []; - - documentation.forEach(doc => { - const patterns = doc.content.match(/❌.*$/gm); - if (patterns) { - antiPatterns.push(...patterns); - } - }); - - return antiPatterns; -} - -function extractTemplates(documentation) { - // Extract code blocks marked as templates - const templates = {}; - - documentation.forEach(doc => { - if (doc.path.includes('snippets')) { - const codeBlocks = doc.content.match(/```cfm(.*?)```/gs); - if (codeBlocks) { - templates[doc.component] = codeBlocks; - } - } - }); - - return templates; -} -``` - -## Error Recovery Documentation Loading - -### Documentation Re-consultation on Errors -```javascript -function loadErrorRecoveryDocumentation(errorType, originalComponents) { - const recoveryDocs = []; - - switch (errorType) { - case 'mixed_arguments': - recoveryDocs.push('.ai/wheels/troubleshooting/common-errors.md'); - recoveryDocs.push('.ai/wheels/patterns/validation-templates.md'); - break; - - case 'query_array_confusion': - recoveryDocs.push('.ai/wheels/models/data-handling.md'); - recoveryDocs.push('.ai/wheels/views/data-handling.md'); - recoveryDocs.push('.ai/cfml/control-flow/loops.md'); - break; - - case 'route_conflicts': - recoveryDocs.push('.ai/wheels/configuration/routing.md'); - recoveryDocs.push('.ai/wheels/core-concepts/routing/patterns.md'); - break; - - case 'validation_failures': - recoveryDocs.push('.ai/wheels/models/validations.md'); - recoveryDocs.push('.ai/wheels/database/validations/'); - break; - } - - // Load original component documentation as backup - recoveryDocs.push(...getComponentDocumentation(originalComponents)); - - return recoveryDocs; -} -``` - -## Usage Guidelines - -### When to Trigger Documentation Loading - -1. **Before any code generation**: Always load documentation first -2. **On error encounters**: Load error-specific documentation -3. **When switching component types**: Load new component documentation -4. **For complex tasks**: Load additional advanced documentation - -### Documentation Loading Optimization - -1. **Cache frequently used documentation**: Keep anti-pattern docs in memory -2. **Progressive loading**: Load basic docs first, advanced docs as needed -3. **Context-aware caching**: Remember documentation preferences per project -4. **Lazy loading**: Load reference documentation only when specific features are used - -### Integration with Development Workflow - -1. **Pre-implementation phase**: Load all relevant documentation before planning -2. **Implementation phase**: Have documentation context available for reference -3. **Error recovery phase**: Automatically load error-specific documentation -4. **Validation phase**: Use documentation for anti-pattern checking - -This systematic documentation loading strategy ensures that AI assistants always have the right knowledge available at the right time, preventing common errors and ensuring consistent, high-quality implementations that follow established Wheels patterns and best practices. \ No newline at end of file diff --git a/.ai/wheels/workflows/enhanced-mcp-develop-specification.md b/.ai/wheels/workflows/enhanced-mcp-develop-specification.md deleted file mode 100755 index 97ea3f8c88..0000000000 --- a/.ai/wheels/workflows/enhanced-mcp-develop-specification.md +++ /dev/null @@ -1,480 +0,0 @@ -# Enhanced mcp__wheels__develop Workflow Specification - -## Overview - -This specification defines the enhanced `mcp__wheels__develop` tool that provides a comprehensive, systematic development workflow for Wheels applications. The goal is to create a bulletproof development process that produces professional-quality, thoroughly tested features consistently. - -## Core Principles - -1. **Documentation-Driven Development**: Always consult .ai documentation before implementation -2. **Anti-Pattern Prevention**: Systematically prevent common Wheels errors -3. **Comprehensive Testing**: Every button, form, and link must be tested -4. **Template-Based Implementation**: Use established patterns from .ai documentation -5. **Error Recovery**: Intelligent error handling with documentation re-consultation -6. **Quality Assurance**: Built-in validation and security checks - -## Enhanced Workflow Phases - -### Phase 1: Pre-Flight Documentation Loading (2-3 minutes) - -#### 1.1 Critical Error Prevention (MANDATORY FIRST STEP) -**ALWAYS load these files first, regardless of task type:** -- `.ai/wheels/troubleshooting/common-errors.md` -- `.ai/wheels/patterns/validation-templates.md` - -**Purpose**: Prevent the two most common Wheels errors: -- Mixed argument styles (`hasMany("comments", dependent="delete")`) -- Query/Array confusion (`ArrayLen(post.comments())`) - -#### 1.2 Smart Documentation Discovery -**Task Type Detection Algorithm:** -``` -IF task contains ("model", "User", "Post", etc.) → MODEL workflow -IF task contains ("controller", "action", "CRUD") → CONTROLLER workflow -IF task contains ("view", "template", "form") → VIEW workflow -IF task contains ("migration", "database", "table") → MIGRATION workflow -IF task contains ("route", "routing", "URL") → ROUTING workflow -IF multiple types detected → MULTI-COMPONENT workflow -``` - -**Documentation Loading by Task Type:** - -**Model Tasks:** -1. `.ai/wheels/models/architecture.md` - Model structure and fundamentals -2. `.ai/wheels/models/associations.md` - Relationship patterns (CRITICAL) -3. `.ai/wheels/models/validations.md` - Validation methods and patterns -4. `.ai/wheels/models/best-practices.md` - Development guidelines -5. `.ai/wheels/snippets/model-snippets.md` - Code templates - -**Controller Tasks:** -1. `.ai/wheels/controllers/architecture.md` - Controller fundamentals and CRUD -2. `.ai/wheels/controllers/rendering.md` - View rendering and responses -3. `.ai/wheels/controllers/filters.md` - Authentication and authorization -4. `.ai/wheels/controllers/model-interactions.md` - Controller-model patterns -5. `.ai/wheels/snippets/controller-snippets.md` - Code templates - -**View Tasks:** -1. `.ai/wheels/views/data-handling.md` - CRITICAL query vs array patterns -2. `.ai/wheels/views/architecture.md` - View structure and conventions -3. `.ai/wheels/views/forms.md` - Form helpers and limitations (CRITICAL) -4. `.ai/wheels/views/layouts.md` - Layout patterns and inheritance -5. `.ai/wheels/snippets/view-snippets.md` - Code templates - -**Migration Tasks:** -1. `.ai/wheels/database/migrations/creating-migrations.md` - Migration basics -2. `.ai/wheels/database/migrations/column-types.md` - Column types -3. `.ai/wheels/database/migrations/advanced-operations.md` - Complex operations - -#### 1.3 Project Context Loading -- Current project structure analysis -- Existing models, controllers, views inventory -- Current route configuration -- Migration status and history -- Test coverage assessment - -#### 1.4 Pattern Recognition -- Identify existing code patterns in the project -- Detect argument style consistency (named vs positional) -- Catalog existing associations and validations -- Note naming conventions already in use - -### Phase 2: Intelligent Analysis & Planning (3-5 minutes) - -#### 2.1 Requirement Analysis -**Natural Language Processing:** -- Parse user request for specific components needed -- Identify CRUD operations required -- Detect authentication/authorization needs -- Identify form and validation requirements -- Recognize testing scenarios mentioned - -**Component Mapping:** -``` -"blog with posts and comments" → - Models: Post, Comment - Controllers: PostsController, CommentsController - Views: posts/index, posts/show, comments/_form - Migrations: CreatePosts, CreateComments - Tests: Post model, Comment model, integration tests - Browser Tests: Navigation, CRUD flows, form submissions -``` - -#### 2.2 Dependency Analysis -- Model association requirements -- Controller filter needs -- Route configuration changes -- Migration sequence planning -- Asset and layout dependencies - -#### 2.3 Anti-Pattern Pre-validation -**Check planned approach against common errors:** -- Will any associations use mixed argument styles? -- Are there any plans to use ArrayLen() on model results? -- Will naming conventions be consistent? -- Are all routes following RESTful patterns? - -#### 2.4 Browser Test Scenario Planning -**Comprehensive User Flow Mapping:** -- **Navigation Flows**: Every menu, link, button pathway -- **CRUD Flows**: Create, read, update, delete for each model -- **Form Flows**: Every form submission, validation scenario -- **Authentication Flows**: Login, logout, access control -- **Error Flows**: 404s, validation failures, edge cases -- **Responsive Flows**: Mobile, tablet, desktop layouts - -### Phase 3: Template-Driven Systematic Implementation (5-15 minutes) - -#### 3.1 Code Generation with Templates -**Template Selection Process:** -1. Load appropriate code template from `.ai/wheels/snippets/` -2. Verify template matches current project patterns -3. Apply consistent argument style (named vs positional) -4. Inject project-specific naming conventions -5. Add validation and security patterns - -**Template Application Examples:** -```cfm -// Model Template (from .ai documentation) -component extends="Model" { - function config() { - // Consistent named arguments throughout - hasMany(name="comments", dependent="delete"); - belongsTo(name="user"); - validatesPresenceOf(properties="title,content"); - - // Security validations - validatesLengthOf(property="title", minimum=5, maximum=200); - validatesFormatOf(property="slug", regEx="^[a-z0-9-]+$"); - } -} -``` - -#### 3.2 Incremental Validation -**After each component generation:** -1. Syntax validation using Wheels parser -2. Anti-pattern detection scan -3. Consistency check against existing code -4. Security pattern verification -5. Template compliance validation - -#### 3.3 Error Recovery System -**If generation fails:** -1. **Step 1**: Re-read relevant documentation section -2. **Step 2**: Try alternative template or pattern -3. **Step 3**: Simplify approach and retry -4. **Step 4**: Log error pattern for future prevention -5. **Step 5**: Request human intervention if needed - -**Common Recovery Patterns:** -- Mixed arguments → Convert to consistent style -- Query/Array confusion → Use .recordCount and proper loops -- Route conflicts → Adjust route ordering -- Validation failures → Add missing required fields - -### Phase 4: Multi-Level Testing Framework (3-8 minutes) - -#### 4.1 Unit Testing (Models) -**Automatic test generation for each model:** -```cfm -component extends="wheels.Testbox" { - function run() { - describe("User Model", function() { - it("should validate required fields", function() { - var user = model("User").new(); - expect(user.valid()).toBeFalse(); - expect(arrayLen(user.allErrors())).toBeGT(0); - }); - - it("should create valid user", function() { - var userData = { - firstname = "John", - lastname = "Doe", - email = "john@example.com" - }; - var user = model("User").create(userData); - expect(user.valid()).toBeTrue(); - }); - }); - } -} -``` - -#### 4.2 Integration Testing (Controllers) -**Automatic controller test generation:** -- Test each CRUD action -- Verify authentication filters -- Test parameter validation -- Check response formats (HTML/JSON) -- Validate redirect behaviors - -#### 4.3 Migration Testing -- Test migration up() and down() methods -- Verify data integrity after migrations -- Test rollback scenarios -- Validate foreign key constraints - -#### 4.4 Syntax and Configuration Testing -- Wheels syntax validation -- Route configuration testing -- Application startup testing -- Framework setting validation - -### Phase 5: Comprehensive Browser Testing Automation (5-10 minutes) - -#### 5.1 Server Status Verification -```javascript -// Verify development server is running -mcp__wheels__server(action="status") - -// If not running, start server -if (serverNotRunning) { - mcp__wheels__server(action="start") - wait(5000) // Allow startup time -} -``` - -#### 5.2 Homepage and Navigation Testing -```javascript -// Navigate to application -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT") - -// Take homepage screenshot -mcp__puppeteer__puppeteer_screenshot(name="01_homepage", width=1200, height=800) - -// Test all navigation links -document.querySelectorAll('nav a, .menu a').forEach(async (link, index) => { - await mcp__puppeteer__puppeteer_click(selector=`nav a:nth-child(${index+1})`) - await mcp__puppeteer__puppeteer_screenshot(name=`02_nav_${index}`, width=1200, height=800) -}) -``` - -#### 5.3 CRUD Operation Testing -**For each model (e.g., Post):** -```javascript -// Test index page -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts") -mcp__puppeteer__puppeteer_screenshot(name="posts_index") - -// Test new/create flow -mcp__puppeteer__puppeteer_click(selector="a:contains('New Post')") -mcp__puppeteer__puppeteer_screenshot(name="posts_new_form") - -mcp__puppeteer__puppeteer_fill(selector="input[name='post[title]']", value="Test Post") -mcp__puppeteer__puppeteer_fill(selector="textarea[name='post[content]']", value="Test content") -mcp__puppeteer__puppeteer_click(selector="input[type='submit']") -mcp__puppeteer__puppeteer_screenshot(name="posts_created") - -// Test show page -mcp__puppeteer__puppeteer_click(selector="article:first-child h2 a") -mcp__puppeteer__puppeteer_screenshot(name="posts_show") - -// Test edit flow -mcp__puppeteer__puppeteer_click(selector="a:contains('Edit')") -mcp__puppeteer__puppeteer_screenshot(name="posts_edit_form") - -mcp__puppeteer__puppeteer_fill(selector="input[name='post[title]']", value="Updated Test Post") -mcp__puppeteer__puppeteer_click(selector="input[type='submit']") -mcp__puppeteer__puppeteer_screenshot(name="posts_updated") -``` - -#### 5.4 Form Validation Testing -```javascript -// Test validation errors -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts/new") -mcp__puppeteer__puppeteer_click(selector="input[type='submit']") // Submit empty form -mcp__puppeteer__puppeteer_screenshot(name="validation_errors") - -// Verify error messages are displayed -const errors = await mcp__puppeteer__puppeteer_evaluate({ - script: "document.querySelectorAll('.error, .alert-danger').length" -}) -expect(errors).toBeGT(0) -``` - -#### 5.5 Interactive Element Testing -```javascript -// Test JavaScript/Alpine.js/HTMX functionality -mcp__puppeteer__puppeteer_click(selector="button[x-on\\:click], [hx-get], .btn-js") -mcp__puppeteer__puppeteer_screenshot(name="interactive_elements") - -// Test modal/dropdown functionality -mcp__puppeteer__puppeteer_click(selector="[data-modal-trigger], .dropdown-toggle") -mcp__puppeteer__puppeteer_screenshot(name="modal_dropdown") -``` - -#### 5.6 Responsive Design Testing -```javascript -// Test mobile viewport -mcp__puppeteer__puppeteer_screenshot(name="mobile_view", width=375, height=667) - -// Test tablet viewport -mcp__puppeteer__puppeteer_screenshot(name="tablet_view", width=768, height=1024) - -// Test desktop viewport -mcp__puppeteer__puppeteer_screenshot(name="desktop_view", width=1920, height=1080) -``` - -#### 5.7 Error Scenario Testing -```javascript -// Test 404 handling -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/nonexistent") -mcp__puppeteer__puppeteer_screenshot(name="404_error") - -// Test authentication redirects -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/admin") -mcp__puppeteer__puppeteer_screenshot(name="auth_redirect") -``` - -### Phase 6: Quality Assurance & Reporting (2-3 minutes) - -#### 6.1 Anti-Pattern Detection -**Automated scanning for common errors:** -```bash -# Check for mixed argument styles -grep -r "hasMany(\"[^\"]*\",[[:space:]]*[a-zA-Z]" app/models/ - -# Check for query/array confusion -grep -r "ArrayLen(" app/views/ -grep -r "for.*in.*\(\)" app/views/ - -# Check for incorrect naming conventions -find app/models/ -name "*s.cfc" # Should find no plural model names -find app/controllers/ -name "*[^s]Controller.cfc" # Should find no singular controllers -``` - -#### 6.2 Security Review -**Automatic security validation:** -- CSRF protection verification -- Parameter validation checks -- Input sanitization validation -- Authentication filter coverage -- SQL injection prevention checks - -#### 6.3 Performance Analysis -- Query count analysis (N+1 detection) -- Asset optimization validation -- Caching strategy verification -- Database index usage review - -#### 6.4 Documentation Compliance -**Verify implementation matches documentation:** -- Compare against templates in `.ai/wheels/snippets/` -- Check adherence to patterns in `.ai/wheels/patterns/` -- Validate against best practices in `.ai/wheels/*/best-practices.md` - -#### 6.5 Comprehensive Report Generation -**Final report includes:** -- ✅ **Components Created**: List of all generated files -- ✅ **Tests Passed**: Unit, integration, browser test results -- ✅ **Screenshots**: Evidence of all user flows working -- ✅ **Security Checks**: CSRF, validation, authentication status -- ✅ **Performance Metrics**: Query counts, load times, optimization -- ✅ **Anti-Pattern Status**: Confirmation of error prevention -- ⚠️ **Issues Found**: Any problems requiring attention -- 📋 **Next Steps**: Recommended follow-up tasks - -## Error Recovery and Fallback Mechanisms - -### Documentation Re-consultation -**If errors occur during any phase:** -1. **Identify error type** (syntax, logic, pattern, security) -2. **Load relevant documentation** from `.ai` folder based on error -3. **Apply documented solution** or alternative pattern -4. **Retry operation** with corrected approach -5. **Log pattern** for future prevention - -### Common Error Recovery Flows - -#### Mixed Argument Error Recovery -``` -Error: "Missing argument name" detected -→ Load: .ai/wheels/troubleshooting/common-errors.md -→ Identify: Mixed argument pattern -→ Fix: Convert all to named arguments OR all to positional -→ Retry: Code generation with consistent style -→ Validate: Syntax check passes -``` - -#### Query/Array Confusion Recovery -``` -Error: ArrayLen() on query object detected -→ Load: .ai/wheels/models/data-handling.md -→ Identify: Query vs Array confusion -→ Fix: Use .recordCount for count, proper loop syntax -→ Retry: View generation with correct patterns -→ Validate: Browser test confirms functionality -``` - -### Progressive Fallback Strategy -1. **Template-based solution** (primary) -2. **Alternative pattern** from documentation -3. **Simplified approach** (remove complex features) -4. **Manual intervention request** (last resort) - -## Success Criteria - -### Feature is complete when ALL of the following are true: -- [ ] ✅ All relevant `.ai` documentation was consulted -- [ ] ✅ No anti-patterns detected in generated code -- [ ] ✅ All unit tests pass -- [ ] ✅ All integration tests pass -- [ ] ✅ All browser tests pass -- [ ] ✅ Every button, form, and link has been tested -- [ ] ✅ Responsive design works on mobile, tablet, desktop -- [ ] ✅ Security validations are in place -- [ ] ✅ Performance is acceptable -- [ ] ✅ Error scenarios are handled properly -- [ ] ✅ Screenshot evidence exists for all user flows -- [ ] ✅ Implementation follows Wheels conventions - -### Quality Gates - -**No feature may be marked complete if:** -- Any mixed argument styles exist -- Any ArrayLen() calls on model associations exist -- Any browser test fails -- Any security check fails -- Any anti-pattern is detected -- Documentation wasn't consulted for the relevant component type - -## Implementation Priority - -### Phase 1 Implementation (Core Workflow) -1. Enhanced documentation loading system -2. Anti-pattern prevention integration -3. Template-driven code generation -4. Basic browser testing automation - -### Phase 2 Implementation (Advanced Features) -1. Intelligent error recovery -2. Comprehensive testing automation -3. Quality assurance integration -4. Performance analysis - -### Phase 3 Implementation (Polish & Optimization) -1. Advanced reporting -2. Pattern learning system -3. Custom template generation -4. Workflow optimization - -## Integration with Existing Systems - -### MCP Tool Integration -- Leverage existing `mcp__wheels__*` tools -- Enhance with systematic workflow orchestration -- Add documentation loading capabilities -- Integrate browser testing automation - -### Documentation System Integration -- Systematically load `.ai` documentation -- Apply patterns from `.ai/wheels/snippets/` -- Validate against `.ai/wheels/patterns/` -- Reference `.ai/wheels/troubleshooting/` - -### Testing Framework Integration -- Extend TestBox integration -- Add browser testing with Puppeteer -- Integrate with existing test commands -- Add automated test generation - -This specification provides a comprehensive framework for creating a robust, systematic development workflow that produces professional-quality, thoroughly tested Wheels applications consistently. \ No newline at end of file diff --git a/.ai/wheels/workflows/intelligent-analysis-planning-engine.md b/.ai/wheels/workflows/intelligent-analysis-planning-engine.md deleted file mode 100755 index d7eaff8fe0..0000000000 --- a/.ai/wheels/workflows/intelligent-analysis-planning-engine.md +++ /dev/null @@ -1,1009 +0,0 @@ -# Intelligent Analysis and Planning Engine - -## Overview - -This engine provides automated analysis and planning capabilities for the enhanced `mcp__wheels__develop` workflow. It analyzes natural language requirements, maps them to Wheels components, identifies dependencies, and creates detailed implementation plans with comprehensive testing scenarios. - -## Core Analysis Components - -### 1. Natural Language Requirement Analysis - -#### Requirement Parser Engine -```javascript -class RequirementAnalyzer { - analyzeRequirement(userInput) { - return { - intent: this.extractIntent(userInput), - entities: this.extractEntities(userInput), - actions: this.extractActions(userInput), - constraints: this.extractConstraints(userInput), - complexity: this.assessComplexity(userInput) - }; - } - - extractIntent(input) { - const intents = { - 'create': ['create', 'build', 'make', 'add', 'implement', 'develop'], - 'modify': ['update', 'change', 'modify', 'edit', 'enhance'], - 'fix': ['fix', 'debug', 'resolve', 'correct', 'repair'], - 'extend': ['extend', 'expand', 'improve', 'upgrade'] - }; - - for (const [intent, keywords] of Object.entries(intents)) { - if (keywords.some(keyword => input.toLowerCase().includes(keyword))) { - return intent; - } - } - return 'create'; // default - } - - extractEntities(input) { - const entities = { - models: [], - controllers: [], - views: [], - features: [], - relationships: [] - }; - - // Model detection patterns - const modelPatterns = [ - /(?:create|build|add).*?(user|post|comment|product|order|category|article|page)s?/gi, - /(user|post|comment|product|order|category|article|page)(?:\s+model)?/gi - ]; - - modelPatterns.forEach(pattern => { - const matches = input.match(pattern); - if (matches) { - matches.forEach(match => { - const model = this.extractModelName(match); - if (model && !entities.models.includes(model)) { - entities.models.push(model); - } - }); - } - }); - - // Feature detection - const featurePatterns = { - 'authentication': ['login', 'signin', 'signup', 'register', 'auth', 'user auth'], - 'commenting': ['comment', 'comments', 'commenting system'], - 'search': ['search', 'find', 'filter'], - 'pagination': ['page', 'paginate', 'pagination'], - 'admin': ['admin', 'dashboard', 'management'], - 'api': ['api', 'rest', 'json', 'endpoint'], - 'email': ['email', 'mail', 'notification', 'notify'], - 'file_upload': ['upload', 'file', 'image', 'attachment'] - }; - - for (const [feature, keywords] of Object.entries(featurePatterns)) { - if (keywords.some(keyword => input.toLowerCase().includes(keyword))) { - entities.features.push(feature); - } - } - - // Relationship detection - const relationshipPatterns = [ - /(\w+)\s+(?:has|have)\s+(?:many\s+)?(\w+)/gi, - /(\w+)\s+(?:belongs?\s+to|owned\s+by)\s+(\w+)/gi, - /(\w+)\s+(?:can\s+have|contains?)\s+(\w+)/gi - ]; - - relationshipPatterns.forEach(pattern => { - const matches = [...input.matchAll(pattern)]; - matches.forEach(match => { - entities.relationships.push({ - from: this.normalizeModelName(match[1]), - to: this.normalizeModelName(match[2]), - type: this.inferRelationshipType(match[0]) - }); - }); - }); - - return entities; - } - - extractActions(input) { - const actions = []; - const actionPatterns = { - 'crud': ['create', 'read', 'update', 'delete', 'list', 'show', 'edit'], - 'auth': ['login', 'logout', 'register', 'signin', 'signup'], - 'admin': ['manage', 'moderate', 'approve', 'reject'], - 'search': ['search', 'filter', 'sort', 'find'], - 'social': ['like', 'share', 'follow', 'subscribe'] - }; - - for (const [category, keywords] of Object.entries(actionPatterns)) { - if (keywords.some(keyword => input.toLowerCase().includes(keyword))) { - actions.push(category); - } - } - - return actions; - } - - assessComplexity(input) { - let score = 0; - const text = input.toLowerCase(); - - // Model complexity - const modelCount = (text.match(/\b(user|post|comment|product|order|category|article|page)\b/g) || []).length; - score += modelCount * 2; - - // Feature complexity - const complexFeatures = ['auth', 'admin', 'api', 'search', 'payment', 'email']; - score += complexFeatures.filter(feature => text.includes(feature)).length * 3; - - // Relationship complexity - const relationshipIndicators = ['has many', 'belongs to', 'through', 'polymorphic']; - score += relationshipIndicators.filter(indicator => text.includes(indicator)).length * 2; - - // UI complexity - const uiIndicators = ['responsive', 'mobile', 'dashboard', 'admin', 'ajax']; - score += uiIndicators.filter(indicator => text.includes(indicator)).length * 1; - - if (score <= 5) return 'simple'; - if (score <= 15) return 'moderate'; - return 'complex'; - } -} -``` - -### 2. Component Mapping Engine - -#### Component Dependency Mapper -```javascript -class ComponentMapper { - mapRequirementsToComponents(analyzedRequirements) { - const components = { - models: this.generateModelSpecs(analyzedRequirements), - controllers: this.generateControllerSpecs(analyzedRequirements), - views: this.generateViewSpecs(analyzedRequirements), - migrations: this.generateMigrationSpecs(analyzedRequirements), - routes: this.generateRouteSpecs(analyzedRequirements), - tests: this.generateTestSpecs(analyzedRequirements) - }; - - return this.resolveDependencies(components); - } - - generateModelSpecs(requirements) { - const models = []; - - requirements.entities.models.forEach(modelName => { - const model = { - name: this.singularize(modelName), - className: this.pascalCase(this.singularize(modelName)), - fileName: `${this.pascalCase(this.singularize(modelName))}.cfc`, - attributes: this.inferModelAttributes(modelName, requirements), - associations: this.inferModelAssociations(modelName, requirements), - validations: this.inferModelValidations(modelName, requirements), - features: this.inferModelFeatures(modelName, requirements) - }; - - models.push(model); - }); - - // Add implied models - if (requirements.entities.features.includes('authentication')) { - if (!models.find(m => m.name === 'user')) { - models.push(this.generateUserModel()); - } - } - - return models; - } - - inferModelAttributes(modelName, requirements) { - const baseAttributes = { - 'user': ['firstName', 'lastName', 'email', 'password'], - 'post': ['title', 'content', 'published'], - 'comment': ['content', 'authorName', 'authorEmail'], - 'product': ['name', 'description', 'price', 'inStock'], - 'order': ['total', 'status', 'orderDate'], - 'category': ['name', 'description', 'slug'] - }; - - let attributes = baseAttributes[modelName.toLowerCase()] || ['name']; - - // Add common attributes based on features - if (requirements.entities.features.includes('admin')) { - attributes.push('createdAt', 'updatedAt'); - } - - if (modelName.toLowerCase() === 'post' && requirements.entities.features.includes('commenting')) { - // Comments will be handled via association - } - - return attributes.map(attr => ({ - name: attr, - type: this.inferAttributeType(attr), - required: this.isAttributeRequired(attr), - validations: this.getAttributeValidations(attr) - })); - } - - inferModelAssociations(modelName, requirements) { - const associations = []; - - // Explicit relationships from requirement analysis - requirements.entities.relationships.forEach(rel => { - if (rel.from.toLowerCase() === modelName.toLowerCase()) { - associations.push({ - type: 'hasMany', - target: this.pluralize(rel.to), - dependent: 'delete' - }); - } - if (rel.to.toLowerCase() === modelName.toLowerCase()) { - associations.push({ - type: 'belongsTo', - target: rel.from - }); - } - }); - - // Implied relationships - const impliedAssociations = { - 'comment': [ - { type: 'belongsTo', target: 'post' }, - { type: 'belongsTo', target: 'user', optional: true } - ], - 'post': [ - { type: 'hasMany', target: 'comments', dependent: 'delete' }, - { type: 'belongsTo', target: 'user' }, - { type: 'belongsTo', target: 'category', optional: true } - ], - 'order': [ - { type: 'belongsTo', target: 'user' }, - { type: 'hasMany', target: 'orderItems', dependent: 'delete' } - ] - }; - - if (impliedAssociations[modelName.toLowerCase()]) { - associations.push(...impliedAssociations[modelName.toLowerCase()]); - } - - return associations; - } - - generateControllerSpecs(requirements) { - const controllers = []; - - // Generate controller for each model - requirements.entities.models.forEach(modelName => { - const controller = { - name: `${this.pluralize(modelName)}Controller`, - fileName: `${this.pascalCase(this.pluralize(modelName))}Controller.cfc`, - model: this.singularize(modelName), - actions: this.inferControllerActions(modelName, requirements), - filters: this.inferControllerFilters(modelName, requirements), - features: this.inferControllerFeatures(modelName, requirements) - }; - - controllers.push(controller); - }); - - // Add feature-specific controllers - if (requirements.entities.features.includes('authentication')) { - controllers.push(this.generateSessionsController()); - } - - if (requirements.entities.features.includes('admin')) { - controllers.push(this.generateAdminController()); - } - - return controllers; - } - - inferControllerActions(modelName, requirements) { - const baseActions = ['index', 'show', 'new', 'create', 'edit', 'update']; - - // Add delete action if not restricted - if (!this.isDeleteRestricted(modelName)) { - baseActions.push('delete'); - } - - // Add feature-specific actions - if (requirements.entities.features.includes('search')) { - baseActions.push('search'); - } - - if (requirements.entities.features.includes('api')) { - // API actions might differ - return ['index', 'show', 'create', 'update', 'delete']; - } - - return baseActions; - } -} -``` - -### 3. Dependency Analysis Engine - -#### Dependency Resolution -```javascript -class DependencyAnalyzer { - analyzeDependencies(components) { - return { - implementationOrder: this.calculateImplementationOrder(components), - migrationSequence: this.calculateMigrationSequence(components), - testDependencies: this.calculateTestDependencies(components), - conflicts: this.detectConflicts(components) - }; - } - - calculateImplementationOrder(components) { - const order = []; - - // 1. Base models (no dependencies) - const baseModels = components.models.filter(model => - !model.associations.some(assoc => assoc.type === 'belongsTo') - ); - order.push(...baseModels.map(model => ({ type: 'model', name: model.name }))); - - // 2. Dependent models - const dependentModels = components.models.filter(model => - model.associations.some(assoc => assoc.type === 'belongsTo') - ); - order.push(...dependentModels.map(model => ({ type: 'model', name: model.name }))); - - // 3. Controllers - order.push(...components.controllers.map(controller => ({ - type: 'controller', - name: controller.name - }))); - - // 4. Views - order.push(...components.views.map(view => ({ - type: 'view', - name: view.name - }))); - - // 5. Routes (last) - order.push({ type: 'routes', name: 'routes' }); - - return order; - } - - calculateMigrationSequence(components) { - const migrations = []; - - // Base tables first (referenced by foreign keys) - const baseTables = components.models.filter(model => - !model.associations.some(assoc => assoc.type === 'belongsTo') - ); - - baseTables.forEach(model => { - migrations.push({ - name: `Create${model.className}Table`, - model: model.name, - dependencies: [], - order: migrations.length + 1 - }); - }); - - // Dependent tables - const dependentTables = components.models.filter(model => - model.associations.some(assoc => assoc.type === 'belongsTo') - ); - - dependentTables.forEach(model => { - const dependencies = model.associations - .filter(assoc => assoc.type === 'belongsTo') - .map(assoc => assoc.target); - - migrations.push({ - name: `Create${model.className}Table`, - model: model.name, - dependencies: dependencies, - order: migrations.length + 1 - }); - }); - - return migrations; - } - - detectConflicts(components) { - const conflicts = []; - - // Check for naming conflicts - const modelNames = components.models.map(m => m.name.toLowerCase()); - const duplicateModels = modelNames.filter((name, index) => - modelNames.indexOf(name) !== index - ); - - if (duplicateModels.length > 0) { - conflicts.push({ - type: 'naming_conflict', - component: 'models', - message: `Duplicate model names: ${duplicateModels.join(', ')}` - }); - } - - // Check for circular dependencies - const circularDeps = this.detectCircularDependencies(components.models); - if (circularDeps.length > 0) { - conflicts.push({ - type: 'circular_dependency', - component: 'models', - message: `Circular dependencies detected: ${circularDeps.join(' -> ')}` - }); - } - - // Check for missing dependencies - components.models.forEach(model => { - model.associations.forEach(assoc => { - if (assoc.type === 'belongsTo') { - const targetExists = components.models.some(m => - m.name.toLowerCase() === assoc.target.toLowerCase() - ); - if (!targetExists) { - conflicts.push({ - type: 'missing_dependency', - component: 'models', - model: model.name, - missing: assoc.target, - message: `Model ${model.name} references non-existent ${assoc.target}` - }); - } - } - }); - }); - - return conflicts; - } -} -``` - -### 4. Test Scenario Planning Engine - -#### Comprehensive Test Planning -```javascript -class TestScenarioPlanner { - generateTestPlan(components, requirements) { - return { - unitTests: this.planUnitTests(components), - integrationTests: this.planIntegrationTests(components, requirements), - browserTests: this.planBrowserTests(components, requirements), - apiTests: this.planAPITests(components, requirements), - performanceTests: this.planPerformanceTests(components, requirements) - }; - } - - planUnitTests(components) { - const unitTests = []; - - // Model unit tests - components.models.forEach(model => { - unitTests.push({ - type: 'model_unit', - target: model.name, - testFile: `tests/models/${model.className}Test.cfc`, - scenarios: [ - 'validation_tests', - 'association_tests', - 'method_tests', - 'scope_tests' - ], - specificTests: this.generateModelTestScenarios(model) - }); - }); - - // Controller unit tests - components.controllers.forEach(controller => { - unitTests.push({ - type: 'controller_unit', - target: controller.name, - testFile: `tests/controllers/${controller.name}Test.cfc`, - scenarios: [ - 'action_tests', - 'filter_tests', - 'authentication_tests', - 'authorization_tests' - ], - specificTests: this.generateControllerTestScenarios(controller) - }); - }); - - return unitTests; - } - - planBrowserTests(components, requirements) { - const browserTests = []; - - // Navigation testing - browserTests.push({ - category: 'navigation', - scenarios: [ - 'homepage_load', - 'menu_navigation', - 'breadcrumb_navigation', - 'footer_links' - ] - }); - - // CRUD operation testing - components.models.forEach(model => { - if (this.hasPublicInterface(model, components.controllers)) { - browserTests.push({ - category: 'crud', - model: model.name, - scenarios: [ - `${model.name}_index_page`, - `${model.name}_show_page`, - `${model.name}_create_form`, - `${model.name}_edit_form`, - `${model.name}_delete_action` - ] - }); - } - }); - - // Feature-specific browser tests - if (requirements.entities.features.includes('authentication')) { - browserTests.push({ - category: 'authentication', - scenarios: [ - 'user_registration', - 'user_login', - 'user_logout', - 'password_reset', - 'unauthorized_access' - ] - }); - } - - if (requirements.entities.features.includes('search')) { - browserTests.push({ - category: 'search', - scenarios: [ - 'search_form_submission', - 'search_results_display', - 'empty_search_results', - 'search_filters' - ] - }); - } - - // Responsive design testing - browserTests.push({ - category: 'responsive', - scenarios: [ - 'mobile_layout', - 'tablet_layout', - 'desktop_layout', - 'mobile_navigation' - ] - }); - - return browserTests; - } - - generateModelTestScenarios(model) { - const scenarios = []; - - // Validation tests - model.validations.forEach(validation => { - scenarios.push({ - test: `should_validate_${validation.type}_for_${validation.field}`, - type: 'validation', - validation: validation - }); - }); - - // Association tests - model.associations.forEach(association => { - scenarios.push({ - test: `should_have_${association.type}_${association.target}`, - type: 'association', - association: association - }); - }); - - // CRUD tests - scenarios.push( - { test: 'should_create_valid_record', type: 'crud', action: 'create' }, - { test: 'should_update_record', type: 'crud', action: 'update' }, - { test: 'should_delete_record', type: 'crud', action: 'delete' }, - { test: 'should_find_records', type: 'crud', action: 'find' } - ); - - return scenarios; - } -} -``` - -### 5. Risk Assessment Engine - -#### Risk Analysis and Mitigation -```javascript -class RiskAssessmentEngine { - assessRisks(components, requirements, dependencies) { - return { - technicalRisks: this.assessTechnicalRisks(components, dependencies), - complexityRisks: this.assessComplexityRisks(requirements), - securityRisks: this.assessSecurityRisks(components, requirements), - performanceRisks: this.assessPerformanceRisks(components), - maintenanceRisks: this.assessMaintenanceRisks(components) - }; - } - - assessTechnicalRisks(components, dependencies) { - const risks = []; - - // Circular dependency risk - if (dependencies.conflicts.some(c => c.type === 'circular_dependency')) { - risks.push({ - level: 'high', - type: 'circular_dependency', - description: 'Circular dependencies detected between models', - mitigation: 'Refactor associations to break circular references', - impact: 'Could prevent application startup' - }); - } - - // Complex association risk - const complexModels = components.models.filter(m => m.associations.length > 5); - if (complexModels.length > 0) { - risks.push({ - level: 'medium', - type: 'complex_associations', - description: `Models with many associations: ${complexModels.map(m => m.name).join(', ')}`, - mitigation: 'Consider breaking down into smaller models or using polymorphic associations', - impact: 'Increased complexity and potential performance issues' - }); - } - - // Missing validation risk - const unvalidatedModels = components.models.filter(m => m.validations.length === 0); - if (unvalidatedModels.length > 0) { - risks.push({ - level: 'medium', - type: 'missing_validations', - description: `Models without validations: ${unvalidatedModels.map(m => m.name).join(', ')}`, - mitigation: 'Add appropriate validations for data integrity', - impact: 'Data integrity issues and security vulnerabilities' - }); - } - - return risks; - } - - assessSecurityRisks(components, requirements) { - const risks = []; - - // Authentication risk - if (requirements.entities.features.includes('authentication')) { - const userModel = components.models.find(m => m.name.toLowerCase() === 'user'); - if (userModel && !userModel.attributes.some(a => a.name === 'password')) { - risks.push({ - level: 'high', - type: 'authentication_security', - description: 'User model lacks password field', - mitigation: 'Add password field with proper hashing', - impact: 'Authentication system will not work' - }); - } - } - - // Admin interface risk - if (requirements.entities.features.includes('admin')) { - const adminController = components.controllers.find(c => c.name.includes('Admin')); - if (adminController && !adminController.filters.includes('authentication')) { - risks.push({ - level: 'high', - type: 'admin_security', - description: 'Admin controller lacks authentication filter', - mitigation: 'Add authentication and authorization filters', - impact: 'Unauthorized access to admin functionality' - }); - } - } - - // CSRF risk - const formsPresent = components.views.some(v => v.features.includes('forms')); - if (formsPresent) { - risks.push({ - level: 'medium', - type: 'csrf_protection', - description: 'Forms present without CSRF protection verification', - mitigation: 'Ensure CSRF protection is enabled in all forms', - impact: 'Cross-site request forgery vulnerabilities' - }); - } - - return risks; - } -} -``` - -### 6. Implementation Planning Engine - -#### Detailed Implementation Plans -```javascript -class ImplementationPlanner { - createImplementationPlan(components, dependencies, risks, requirements) { - return { - phases: this.planImplementationPhases(components, dependencies), - timeline: this.estimateTimeline(components, requirements.complexity), - resources: this.identifyRequiredResources(components, requirements), - milestones: this.defineMilestones(components), - riskMitigation: this.planRiskMitigation(risks), - qualityGates: this.defineQualityGates(components) - }; - } - - planImplementationPhases(components, dependencies) { - const phases = []; - - // Phase 1: Foundation - phases.push({ - name: 'Foundation Setup', - description: 'Set up basic models and migrations', - duration: '15-30 minutes', - components: dependencies.implementationOrder.filter(c => - c.type === 'model' && !this.hasComplexDependencies(c, dependencies) - ), - deliverables: [ - 'Base model files created', - 'Database migrations created', - 'Basic validations implemented', - 'Unit tests created' - ], - qualityGates: [ - 'All models pass syntax validation', - 'All migrations run successfully', - 'All unit tests pass' - ] - }); - - // Phase 2: Core Functionality - phases.push({ - name: 'Core Functionality', - description: 'Implement controllers and basic views', - duration: '20-45 minutes', - components: [ - ...dependencies.implementationOrder.filter(c => c.type === 'controller'), - ...dependencies.implementationOrder.filter(c => c.type === 'view') - ], - deliverables: [ - 'Controller actions implemented', - 'Basic views created', - 'Routes configured', - 'Integration tests created' - ], - qualityGates: [ - 'All routes respond correctly', - 'All CRUD operations work', - 'All forms submit successfully' - ] - }); - - // Phase 3: Advanced Features - phases.push({ - name: 'Advanced Features', - description: 'Implement authentication, search, and special features', - duration: '15-30 minutes', - components: this.getAdvancedFeatureComponents(components), - deliverables: [ - 'Authentication system implemented', - 'Search functionality added', - 'Admin interface created', - 'Feature tests created' - ], - qualityGates: [ - 'Authentication works correctly', - 'All features are accessible', - 'Security measures are in place' - ] - }); - - // Phase 4: Testing and Polish - phases.push({ - name: 'Testing and Polish', - description: 'Comprehensive testing and final adjustments', - duration: '10-20 minutes', - components: [], - deliverables: [ - 'All browser tests pass', - 'Performance optimizations applied', - 'Error handling implemented', - 'Documentation updated' - ], - qualityGates: [ - 'All tests pass', - 'No anti-patterns detected', - 'Performance meets requirements', - 'Security audit passes' - ] - }); - - return phases; - } - - estimateTimeline(components, complexity) { - const baseTimeEstimates = { - model: 2, // minutes per model - controller: 3, // minutes per controller - view: 2, // minutes per view set - migration: 1, // minutes per migration - test: 1 // minutes per test - }; - - const complexityMultipliers = { - simple: 1.0, - moderate: 1.5, - complex: 2.0 - }; - - let totalTime = 0; - - Object.entries(components).forEach(([type, items]) => { - if (Array.isArray(items)) { - totalTime += items.length * (baseTimeEstimates[type] || 1); - } - }); - - // Apply complexity multiplier - totalTime *= complexityMultipliers[complexity]; - - // Add buffer time - totalTime *= 1.3; - - return { - estimated: `${Math.round(totalTime)} minutes`, - phases: { - foundation: `${Math.round(totalTime * 0.3)} minutes`, - core: `${Math.round(totalTime * 0.4)} minutes`, - advanced: `${Math.round(totalTime * 0.2)} minutes`, - testing: `${Math.round(totalTime * 0.1)} minutes` - } - }; - } - - defineQualityGates(components) { - return { - syntax: { - description: 'All generated code must pass Wheels syntax validation', - automated: true, - blocking: true - }, - antipatterns: { - description: 'No common Wheels anti-patterns detected', - automated: true, - blocking: true, - checks: [ - 'No mixed argument styles', - 'No ArrayLen() on queries', - 'Proper naming conventions', - 'Consistent code patterns' - ] - }, - functionality: { - description: 'All implemented features work correctly', - automated: true, - blocking: true, - checks: [ - 'All routes respond', - 'All forms submit', - 'All CRUD operations work', - 'All validations trigger' - ] - }, - security: { - description: 'Security measures are properly implemented', - automated: true, - blocking: true, - checks: [ - 'CSRF protection enabled', - 'Authentication filters in place', - 'Input validation present', - 'SQL injection prevention' - ] - }, - testing: { - description: 'Comprehensive test coverage achieved', - automated: true, - blocking: false, - checks: [ - 'Unit tests pass', - 'Integration tests pass', - 'Browser tests pass', - 'Performance tests pass' - ] - } - }; - } -} -``` - -## Usage Integration - -### Main Analysis Engine -```javascript -class IntelligentAnalysisEngine { - analyze(userRequirement) { - // Phase 1: Parse requirement - const analyzer = new RequirementAnalyzer(); - const parsedRequirement = analyzer.analyzeRequirement(userRequirement); - - // Phase 2: Map to components - const mapper = new ComponentMapper(); - const components = mapper.mapRequirementsToComponents(parsedRequirement); - - // Phase 3: Analyze dependencies - const dependencyAnalyzer = new DependencyAnalyzer(); - const dependencies = dependencyAnalyzer.analyzeDependencies(components); - - // Phase 4: Plan tests - const testPlanner = new TestScenarioPlanner(); - const testPlan = testPlanner.generateTestPlan(components, parsedRequirement); - - // Phase 5: Assess risks - const riskAssessment = new RiskAssessmentEngine(); - const risks = riskAssessment.assessRisks(components, parsedRequirement, dependencies); - - // Phase 6: Create implementation plan - const planner = new ImplementationPlanner(); - const implementationPlan = planner.createImplementationPlan( - components, - dependencies, - risks, - parsedRequirement - ); - - return { - requirement: parsedRequirement, - components: components, - dependencies: dependencies, - testPlan: testPlan, - risks: risks, - implementationPlan: implementationPlan, - recommendations: this.generateRecommendations(components, risks) - }; - } - - generateRecommendations(components, risks) { - const recommendations = []; - - // High-risk mitigation recommendations - risks.technicalRisks - .filter(risk => risk.level === 'high') - .forEach(risk => { - recommendations.push({ - priority: 'high', - category: 'risk_mitigation', - description: risk.mitigation, - reason: risk.description - }); - }); - - // Best practice recommendations - const modelCount = components.models.length; - if (modelCount > 5) { - recommendations.push({ - priority: 'medium', - category: 'architecture', - description: 'Consider using namespaces or modules to organize models', - reason: `Large number of models (${modelCount}) may become difficult to manage` - }); - } - - // Performance recommendations - const complexModels = components.models.filter(m => m.associations.length > 3); - if (complexModels.length > 0) { - recommendations.push({ - priority: 'medium', - category: 'performance', - description: 'Consider eager loading for models with many associations', - reason: 'Complex associations may cause N+1 query problems' - }); - } - - return recommendations; - } -} -``` - -This intelligent analysis and planning engine provides comprehensive requirement analysis, component mapping, dependency resolution, risk assessment, and detailed implementation planning, ensuring that every development task is thoroughly analyzed and planned before implementation begins. \ No newline at end of file diff --git a/.ai/wheels/workflows/pre-implementation.md b/.ai/wheels/workflows/pre-implementation.md deleted file mode 100755 index a67ac5a9c5..0000000000 --- a/.ai/wheels/workflows/pre-implementation.md +++ /dev/null @@ -1,429 +0,0 @@ -# Pre-Implementation Workflow - -## Description -Mandatory step-by-step workflow that AI assistants MUST follow before writing any Wheels code. This workflow prevents the two most common Wheels errors through systematic documentation consultation and validation. - -## Key Points -- Decision tree for determining documentation requirements -- Mandatory reading lists with specific file paths -- Code templates that must be used as starting points -- Post-implementation validation requirements -- Error prevention through pattern recognition - -## 🚨 CRITICAL: This Workflow is MANDATORY, Not Optional 🚨 - -**VIOLATION OF THIS WORKFLOW WILL RESULT IN BROKEN CODE** - -## Phase 1: Emergency Error Prevention (ALWAYS FIRST) - -### 🛑 STOP: Load Critical Error Documentation -Before ANY code analysis or planning, read these files: - -```bash -MUST READ FIRST: -1. .ai/wheels/troubleshooting/common-errors.md -2. .ai/wheels/patterns/validation-templates.md -``` - -**Purpose:** Prevent the two most common Wheels errors: -1. **Argument mixing** (`hasMany("comments", dependent="delete")`) -2. **Query/Array confusion** (`ArrayLen(model.comments())`) - -### ⚠️ Error Pattern Recognition Training -After reading the error documentation, you MUST be able to identify these patterns: - -**FATAL Pattern #1 - Mixed Arguments:** -```cfm -❌ hasMany("comments", dependent="delete") // WILL BREAK -❌ model("Post").findByKey(params.key, include="comments") // WILL BREAK -❌ renderText("Error", status=404) // WILL BREAK -``` - -**FATAL Pattern #2 - Query as Array:** -```cfm -❌ ArrayLen(post.comments()) // WILL BREAK -❌ // WILL BREAK -❌ for (comment in post.comments()) { // MAY BREAK -``` - -## Phase 2: Task Type Identification and Documentation Loading - -### 🔍 Task Type Decision Tree - -Use this decision tree to determine what documentation to load: - -``` -START: What type of code are you writing? -│ -├── Creating/Modifying a Model (*.cfc in /app/models/) -│ └── → Go to MODEL WORKFLOW -│ -├── Creating/Modifying a Controller (*.cfc in /app/controllers/) -│ └── → Go to CONTROLLER WORKFLOW -│ -├── Creating/Modifying a View (*.cfm in /app/views/) -│ └── → Go to VIEW WORKFLOW -│ -├── Creating/Modifying a Migration (*.cfc in /app/migrator/migrations/) -│ └── → Go to MIGRATION WORKFLOW -│ -├── Working with Forms or Form Helpers -│ └── → Go to FORM WORKFLOW -│ -├── Working with Queries or Database Operations -│ └── → Go to QUERY WORKFLOW -│ -├── Working with Associations (hasMany, belongsTo, etc.) -│ └── → Go to ASSOCIATION WORKFLOW -│ -└── Multiple types or complex feature - └── → Go to MULTI-COMPONENT WORKFLOW -``` - -### 📚 Mandatory Reading Lists by Task Type - -#### MODEL WORKFLOW -**Required Reading Order (ALL files MUST be read):** -```bash -1. .ai/wheels/troubleshooting/common-errors.md # Error prevention -2. .ai/wheels/patterns/validation-templates.md # Validation checklist -3. .ai/wheels/database/associations/has-many.md # Association patterns -4. .ai/wheels/core-concepts/mvc-architecture/models.md # Model fundamentals -5. .ai/cfml/components/component-basics.md # CFC syntax -6. .ai/wheels/snippets/model-snippets.md # Code examples -``` - -**Anti-Pattern Checklist Before Writing:** -- [ ] Will NOT mix argument styles in associations -- [ ] Will use SINGULAR naming (User.cfc, not Users.cfc) -- [ ] Will NOT treat associations as arrays -- [ ] Will extend "Model" class -- [ ] Will use proper validation syntax - -#### CONTROLLER WORKFLOW -**Required Reading Order (ALL files MUST be read):** -```bash -1. .ai/wheels/troubleshooting/common-errors.md # Error prevention -2. .ai/wheels/patterns/validation-templates.md # Validation checklist -3. .ai/wheels/controllers/rendering/views.md # View rendering -4. .ai/wheels/controllers/filters/authentication.md # Authentication -5. .ai/wheels/controllers/params/verification.md # Parameter handling -6. .ai/cfml/syntax/cfscript-vs-tags.md # CFScript syntax -7. .ai/wheels/snippets/controller-snippets.md # Code examples -``` - -**Anti-Pattern Checklist Before Writing:** -- [ ] Will NOT mix argument styles in model calls -- [ ] Will use PLURAL naming (PostsController.cfc) -- [ ] Will NOT treat model results as arrays -- [ ] Will extend "Controller" class -- [ ] Will handle 404s properly - -#### VIEW WORKFLOW -**Required Reading Order (ALL files MUST be read):** -```bash -1. .ai/wheels/troubleshooting/common-errors.md # Error prevention -2. .ai/wheels/patterns/validation-templates.md # Validation checklist -3. .ai/wheels/views/layouts/structure.md # Layout patterns -4. .ai/cfml/control-flow/loops.md # Loop syntax -5. .ai/wheels/views/helpers/forms.md # Form helpers -``` - -**Anti-Pattern Checklist Before Writing:** -- [ ] Will NOT loop queries as arrays -- [ ] Will NOT use ArrayLen() on queries -- [ ] Will use `` syntax -- [ ] Will use `.recordCount` for counts -- [ ] Will properly escape output - -#### MIGRATION WORKFLOW -**Required Reading Order (ALL files MUST be read):** -```bash -1. .ai/wheels/troubleshooting/common-errors.md # Error prevention -2. .ai/wheels/patterns/validation-templates.md # Validation checklist -3. .ai/wheels/database/migrations/creating-migrations.md # Migration basics -4. .ai/wheels/database/migrations/column-types.md # Column types -``` - -**Anti-Pattern Checklist Before Writing:** -- [ ] Will NOT use complex parameter binding for data seeding -- [ ] Will use direct SQL for data insertion -- [ ] Will wrap operations in transactions -- [ ] Will extend "Migration" class -- [ ] Will implement both up() and down() - -#### ASSOCIATION WORKFLOW -**Required Reading Order (ALL files MUST be read):** -```bash -1. .ai/wheels/troubleshooting/common-errors.md # Error prevention -2. .ai/wheels/patterns/validation-templates.md # Validation checklist -3. .ai/wheels/database/associations/has-many.md # hasMany patterns -4. .ai/wheels/database/associations/belongs-to.md # belongsTo patterns -5. .ai/wheels/database/associations/has-one.md # hasOne patterns -6. .ai/cfml/control-flow/loops.md # Loop syntax for queries -``` - -**Anti-Pattern Checklist Before Writing:** -- [ ] Will NOT mix argument styles in association calls -- [ ] Will NOT treat association results as arrays -- [ ] Will use consistent argument syntax throughout -- [ ] Will understand associations return QUERIES -- [ ] Will use proper loop syntax for query results - -#### FORM WORKFLOW -**Required Reading Order (ALL files MUST be read):** -```bash -1. .ai/wheels/troubleshooting/common-errors.md # Error prevention -2. .ai/wheels/patterns/validation-templates.md # Validation checklist -3. .ai/wheels/views/helpers/forms.md # Form helpers -4. .ai/wheels/security/csrf-protection.md # CSRF protection -``` - -#### QUERY WORKFLOW -**Required Reading Order (ALL files MUST be read):** -```bash -1. .ai/wheels/troubleshooting/common-errors.md # Error prevention -2. .ai/cfml/database/query-basics.md # Query fundamentals -3. .ai/cfml/control-flow/loops.md # Loop syntax -4. .ai/wheels/database/queries/finding-records.md # Finding records -``` - -## Phase 3: Code Template Selection - -### 📋 Mandatory Code Templates - -Based on your task type, you MUST use these templates as starting points: - -#### Model Template (MANDATORY) -```cfm -component extends="Model" { - function config() { - // Choose ONE argument style and stick with it - - // Option 1: ALL NAMED arguments - hasMany(name="comments", dependent="delete"); - belongsTo(name="user"); - validatesPresenceOf(properties="title,content"); - - // Option 2: ALL POSITIONAL arguments - hasMany("comments"); - belongsTo("user"); - validatesPresenceOf("title,content"); - - // NEVER mix styles within the same component - } - - // Custom methods using consistent argument style - function findBySlug(required string slug) { - return findOne(where="slug = '#arguments.slug#'"); - } -} -``` - -#### Controller Template (MANDATORY) -```cfm -component extends="Controller" { - function config() { - // Use consistent argument style - filters(through="authenticate"); - verifies(params="key", paramsTypes="integer"); - } - - function show() { - // ALL NAMED arguments - post = model("Post").findByKey(key=params.key, include="comments"); - - if (!isObject(post)) { - renderText(text="Not found", status=404); - return; - } - - // Association returns QUERY, not array - commentCount = post.comments().recordCount; - } -} -``` - -#### View Template (MANDATORY) -```cfm - - - - - - -

#posts.title#

- -

Comments: #posts.comments().recordCount#

-
- -

No posts found.

-
-
-``` - -#### Migration Template (MANDATORY) -```cfm -component extends="wheels.migrator.Migration" { - function up() { - transaction { - t = createTable(name="posts", force=false); - t.string(columnNames="title", allowNull=false); - t.text(columnNames="content"); - t.timestamps(); - t.create(); - - // Use direct SQL for data seeding - execute("INSERT INTO posts (title, content, createdAt, updatedAt) - VALUES ('Sample Post', 'Content here...', NOW(), NOW())"); - } - } - - function down() { - dropTable("posts"); - } -} -``` - -## Phase 4: Implementation with Continuous Validation - -### 🔄 While Writing Code Checklist - -For EVERY function call you write, check: - -**Argument Consistency:** -- [ ] Are all arguments named? OR are all arguments positional? -- [ ] Am I mixing styles? (If yes, STOP and fix) - -**Data Type Awareness:** -- [ ] Am I calling ArrayLen() on something that might be a query? -- [ ] Am I looping something as an array that might be a query? -- [ ] Am I using .recordCount for query counts? - -**Naming Conventions:** -- [ ] Is my model name singular? (User.cfc, Post.cfc) -- [ ] Is my controller name plural? (UsersController.cfc, PostsController.cfc) - -### 🚨 Emergency Stops During Implementation - -**STOP and consult documentation if you:** -1. Get ANY error about "Missing argument name" -2. Get ANY error about "Can't cast Object type [Query] to Array" -3. Are unsure about argument syntax for any Wheels function -4. Are looping over model results or associations -5. Are counting records or checking if data exists - -**When in doubt:** -1. Re-read `.ai/wheels/troubleshooting/common-errors.md` -2. Check `.ai/wheels/patterns/validation-templates.md` -3. Use the templates above as reference - -## Phase 5: Post-Implementation Validation - -### ✅ MANDATORY Validation Steps - -**BEFORE considering your code complete:** - -1. **Syntax Validation:** - ```bash - wheels server start --validate - ``` - -2. **Test Validation:** - ```bash - wheels test run - ``` - -3. **Anti-Pattern Detection:** - ```bash - # Check for mixed arguments - grep -r "hasMany(\"[^\"]*\",[[:space:]]*[a-zA-Z]" app/models/ - - # Check for query/array confusion - grep -r "ArrayLen(" app/views/ - - # Check for plural model names - find app/models/ -name "*s.cfc" - ``` - -4. **Manual Review Against Templates:** - - Compare your code against the templates above - - Verify all checklists are satisfied - - Ensure consistent argument styles throughout - -### 🚫 Code Review Rejection Criteria - -Your code WILL BE REJECTED if: -- [ ] Any mixed argument styles found -- [ ] Any ArrayLen() calls on associations or model results -- [ ] Any array-style loops on query objects -- [ ] Incorrect naming conventions (plural models, singular controllers) -- [ ] Missing error handling for 404s -- [ ] No transaction wrapping for migrations - -## Emergency Procedures - -### 🆘 If You Encounter Unknown Patterns - -**Step 1:** STOP writing code immediately -**Step 2:** Search for similar patterns in `.ai/wheels/snippets/` -**Step 3:** If not found, search in `.ai/wheels/patterns/` -**Step 4:** If still not found, consult `.ai/wheels/core-concepts/` -**Step 5:** Document new pattern for future reference - -### 🆘 If You Get Errors - -**Step 1:** Check error message against `.ai/wheels/troubleshooting/common-errors.md` -**Step 2:** If error matches known patterns, apply documented solution -**Step 3:** If new error, add to common-errors.md documentation -**Step 4:** Re-run validation workflow - -### 🆘 If Documentation is Missing - -**Use MCP Fallback:** -```bash -mcp resource read wheels://.ai/wheels/troubleshooting/common-errors -mcp resource read wheels://.ai/wheels/patterns/validation-templates -mcp resource read wheels://.ai/wheels/[relevant-path] -``` - -## Workflow Compliance Verification - -### Self-Assessment Checklist - -Before claiming workflow compliance, verify: - -- [ ] I read ALL mandatory documentation for my task type -- [ ] I completed ALL anti-pattern checklists -- [ ] I used the mandatory code templates as starting points -- [ ] I followed continuous validation during implementation -- [ ] I completed ALL post-implementation validation steps -- [ ] I can explain why my code won't trigger the two common errors - -### Peer Review Checklist - -When reviewing AI-generated code, verify: - -- [ ] Documentation reading requirements were met -- [ ] Anti-pattern checklists were completed -- [ ] Code follows mandatory templates -- [ ] No mixed argument styles present -- [ ] No query/array confusion present -- [ ] Proper naming conventions followed -- [ ] Error handling implemented correctly - -## Related Documentation - -- [Common Errors](../troubleshooting/common-errors.md) - CRITICAL first read -- [Validation Templates](../patterns/validation-templates.md) - Mandatory checklists -- [Code Snippets](../snippets/) - Template examples -- [MVC Architecture](../core-concepts/mvc-architecture/) - Framework fundamentals - -## Important Notes - -- This workflow is MANDATORY, not optional -- Each phase must be completed before proceeding to the next -- Skipping steps WILL result in broken code -- New error patterns must be documented immediately -- Workflow compliance is required for all AI-generated code -- This workflow prevents 90%+ of Wheels development errors \ No newline at end of file diff --git a/.ai/wheels/workflows/template-driven-implementation-patterns.md b/.ai/wheels/workflows/template-driven-implementation-patterns.md deleted file mode 100755 index 2edd2df1e9..0000000000 --- a/.ai/wheels/workflows/template-driven-implementation-patterns.md +++ /dev/null @@ -1,849 +0,0 @@ -# Template-Driven Implementation with Error Recovery Patterns - -## Overview - -This document defines the template-driven implementation system for the `/wheels_execute` workflow. It provides code generation patterns, error recovery mechanisms, and quality assurance integration to ensure consistent, high-quality Wheels implementations. - -## Core Implementation Principles - -1. **Template-First Approach**: Always start with proven patterns from `.ai/wheels/snippets/` -2. **Consistency Enforcement**: Maintain argument styles and naming conventions -3. **Progressive Enhancement**: Build complexity incrementally with validation at each step -4. **Error Recovery**: Intelligent fallbacks when generation fails -5. **Quality Validation**: Continuous validation against anti-patterns - -## Template Selection Engine - -### Template Repository Structure -``` -.ai/wheels/snippets/ -├── models/ -│ ├── basic-model-template.cfm -│ ├── user-authentication-template.cfm -│ ├── association-heavy-template.cfm -│ └── validation-intensive-template.cfm -├── controllers/ -│ ├── basic-crud-controller.cfm -│ ├── api-controller-template.cfm -│ ├── admin-controller-template.cfm -│ └── authentication-controller.cfm -├── views/ -│ ├── index-view-template.cfm -│ ├── form-view-template.cfm -│ ├── show-view-template.cfm -│ └── layout-template.cfm -├── migrations/ -│ ├── create-table-migration.cfm -│ ├── add-column-migration.cfm -│ └── create-join-table-migration.cfm -└── tests/ - ├── model-test-template.cfm - ├── controller-test-template.cfm - └── integration-test-template.cfm -``` - -### Template Selection Algorithm -```javascript -class TemplateSelector { - selectTemplate(componentType, features, complexity, existingPatterns) { - const templateMap = { - model: this.selectModelTemplate(features, complexity), - controller: this.selectControllerTemplate(features, complexity), - view: this.selectViewTemplate(features, complexity), - migration: this.selectMigrationTemplate(features, complexity) - }; - - let selectedTemplate = templateMap[componentType]; - - // Adapt template to existing patterns - selectedTemplate = this.adaptToExistingPatterns(selectedTemplate, existingPatterns); - - return selectedTemplate; - } - - selectModelTemplate(features, complexity) { - // Authentication model - if (features.includes('authentication')) { - return 'user-authentication-template.cfm'; - } - - // Complex associations - if (complexity === 'complex' || features.includes('many_associations')) { - return 'association-heavy-template.cfm'; - } - - // Heavy validation requirements - if (features.includes('validation_intensive')) { - return 'validation-intensive-template.cfm'; - } - - // Default basic model - return 'basic-model-template.cfm'; - } - - selectControllerTemplate(features, complexity) { - // API controller - if (features.includes('api')) { - return 'api-controller-template.cfm'; - } - - // Admin controller - if (features.includes('admin')) { - return 'admin-controller-template.cfm'; - } - - // Authentication controller - if (features.includes('authentication')) { - return 'authentication-controller.cfm'; - } - - // Default CRUD controller - return 'basic-crud-controller.cfm'; - } -} -``` - -## Code Generation Patterns - -### Model Generation Template -```cfm - -component extends="Model" { - - function config() { - // Associations - CONSISTENT ARGUMENT STYLE ENFORCED - {{#associations}} - {{#if (eq type "hasMany")}} - hasMany(name="{{target}}"{{#if dependent}}, dependent="{{dependent}}"{{/if}}); - {{/if}} - {{#if (eq type "belongsTo")}} - belongsTo(name="{{target}}"{{#if foreignKey}}, foreignKey="{{foreignKey}}"{{/if}}); - {{/if}} - {{#if (eq type "hasOne")}} - hasOne(name="{{target}}"{{#if dependent}}, dependent="{{dependent}}"{{/if}}); - {{/if}} - {{/associations}} - - // Validations - ANTI-PATTERN PREVENTION - {{#validations}} - {{#if (eq type "presence")}} - validatesPresenceOf(properties="{{properties}}"); - {{/if}} - {{#if (eq type "uniqueness")}} - validatesUniquenessOf(property="{{property}}"{{#if scope}}, scope="{{scope}}"{{/if}}); - {{/if}} - {{#if (eq type "format")}} - validatesFormatOf(property="{{property}}", regEx="{{regex}}"); - {{/if}} - {{#if (eq type "length")}} - validatesLengthOf(property="{{property}}"{{#if minimum}}, minimum={{minimum}}{{/if}}{{#if maximum}}, maximum={{maximum}}{{/if}}); - {{/if}} - {{/validations}} - - // Callbacks - {{#callbacks}} - {{callback}}("{{method}}"); - {{/callbacks}} - - // Security - Always include timestamps for audit trail - timestamps(); - } - - {{#customMethods}} - // Custom finder methods - function findBy{{pascalCase property}}(required {{dataType}} {{property}}) { - return findOne(where="{{snakeCase property}} = :{{property}}", {{property}}=arguments.{{property}}); - } - {{/customMethods}} - - {{#if isUserModel}} - // Authentication methods - function authenticate(required string password) { - return hashPassword(arguments.password) == this.password; - } - - private function hashPassword(required string password) { - return hash(arguments.password & this.salt, "SHA-256"); - } - {{/if}} - - {{#businessMethods}} - // Business logic methods - function {{methodName}}({{#parameters}}{{#unless @first}}, {{/unless}}{{type}} {{name}}{{/parameters}}) { - {{methodBody}} - } - {{/businessMethods}} -} -``` - -### Controller Generation Template -```cfm - -component extends="Controller" { - - function config() { - // Filters - SECURITY FIRST - {{#filters}} - filters(through="{{name}}"{{#if except}}, except="{{except}}"{{/if}}{{#if only}}, only="{{only}}"{{/if}}); - {{/filters}} - - // Parameter verification - INPUT VALIDATION - {{#paramVerifications}} - verifies({{#if only}}only="{{only}}", {{/if}}{{#if except}}except="{{except}}", {{/if}}params="{{params}}", paramsTypes="{{paramsTypes}}"); - {{/paramVerifications}} - - // Content type support - provides("{{contentTypes}}"); - - {{#if csrfProtection}} - // CSRF Protection - SECURITY REQUIREMENT - protectsFromForgery(); - {{/if}} - } - - // Index action - LIST VIEW - function index() { - {{modelVariable}} = model("{{modelName}}").findAll({{#if defaultOrder}}order="{{defaultOrder}}"{{/if}}{{#if includes}}, include="{{includes}}"{{/if}}{{#if pagination}}, page=params.page, perPage={{perPage}}{{/if}}); - - {{#if hasSearch}} - // Search functionality - if (structKeyExists(params, "search") && len(trim(params.search))) { - {{modelVariable}} = model("{{modelName}}").findAll( - where="{{searchFields}} LIKE :search", - search="%#params.search#%"{{#if defaultOrder}}, - order="{{defaultOrder}}"{{/if}} - ); - } - {{/if}} - } - - // Show action - DETAIL VIEW - function show() { - {{singularVariable}} = model("{{modelName}}").findByKey(key=params.key{{#if includes}}, include="{{includes}}"{{/if}}); - - if (!isObject({{singularVariable}})) { - renderText(text="{{modelName}} not found", status=404); - return; - } - } - - // New action - FORM DISPLAY - function new() { - {{singularVariable}} = model("{{modelName}}").new(); - {{#relatedModels}} - {{variable}} = model("{{model}}").findAll({{#if order}}order="{{order}}"{{/if}}); - {{/relatedModels}} - } - - // Create action - FORM PROCESSING - function create() { - {{singularVariable}} = model("{{modelName}}").new({{#if nestedParams}}params.{{nestedParams}}{{else}}params{{/if}}); - - if ({{singularVariable}}.save()) { - redirectTo({{#if redirectRoute}}route="{{redirectRoute}}", key={{singularVariable}}.id{{else}}action="index"{{/if}}, success="{{modelName}} created successfully!"); - } else { - {{#relatedModels}} - {{variable}} = model("{{model}}").findAll({{#if order}}order="{{order}}"{{/if}}); - {{/relatedModels}} - renderView(action="new"); - } - } - - // Edit action - EDIT FORM DISPLAY - function edit() { - {{singularVariable}} = model("{{modelName}}").findByKey(key=params.key); - - if (!isObject({{singularVariable}})) { - redirectTo(action="index", error="{{modelName}} not found"); - return; - } - - {{#relatedModels}} - {{variable}} = model("{{model}}").findAll({{#if order}}order="{{order}}"{{/if}}); - {{/relatedModels}} - } - - // Update action - UPDATE PROCESSING - function update() { - {{singularVariable}} = model("{{modelName}}").findByKey(key=params.key); - - if (!isObject({{singularVariable}})) { - redirectTo(action="index", error="{{modelName}} not found"); - return; - } - - if ({{singularVariable}}.update({{#if nestedParams}}params.{{nestedParams}}{{else}}params{{/if}})) { - redirectTo({{#if redirectRoute}}route="{{redirectRoute}}", key={{singularVariable}}.id{{else}}action="show", key={{singularVariable}}.id{{/if}}, success="{{modelName}} updated successfully!"); - } else { - {{#relatedModels}} - {{variable}} = model("{{model}}").findAll({{#if order}}order="{{order}}"{{/if}}); - {{/relatedModels}} - renderView(action="edit"); - } - } - - {{#if hasDelete}} - // Delete action - DELETION PROCESSING - function delete() { - {{singularVariable}} = model("{{modelName}}").findByKey(key=params.key); - - if (!isObject({{singularVariable}})) { - redirectTo(action="index", error="{{modelName}} not found"); - return; - } - - if ({{singularVariable}}.delete()) { - redirectTo(action="index", success="{{modelName}} deleted successfully!"); - } else { - redirectTo(action="index", error="Unable to delete {{modelName}}"); - } - } - {{/if}} - - {{#authenticationRequired}} - // Authentication filter - private function authenticate() { - if (!session.authenticated) { - redirectTo(route="login", error="Please log in to continue"); - } - } - {{/authenticationRequired}} - - {{#authorizationRequired}} - // Authorization filter - private function authorize() { - if (!session.user.hasRole("{{requiredRole}}")) { - renderText(text="Access denied", status=403); - } - } - {{/authorizationRequired}} - - {{#findFilter}} - // Find resource filter - private function find{{modelName}}() { - {{singularVariable}} = model("{{modelName}}").findByKey(key=params.key); - if (!isObject({{singularVariable}})) { - renderText(text="{{modelName}} not found", status=404); - } - } - {{/findFilter}} -} -``` - -### View Generation Template -```cfm - - -{{#if layoutSpecific}} - -{{/if}} - - -{{#if contentFor}} -#contentFor("title", "{{pageTitle}}")# -{{/if}} - -
-
-
-
-

{{pageTitle}}

- {{#if hasCreateButton}} - #linkTo(route="new{{modelName}}", text="New {{singularDisplayName}}", class="btn btn-primary")# - {{/if}} -
- - {{#if hasSearch}} - -
- #startFormTag(route="{{indexRoute}}", method="get", class="d-flex")# -
- #textField(name="search", value=params.search, placeholder="Search {{pluralDisplayName}}...", class="form-control")# -
- - #endFormTag()# -
- {{/if}} - - - - {{#if displayType eq "table"}} - -
- - - - {{#tableColumns}} - - {{/tableColumns}} - - - - - - - - {{#tableColumns}} - - {{/tableColumns}} - - - - -
{{displayName}}Actions
- {{#if isLink}} - #linkTo(route="{{linkRoute}}", key={{modelVariable}}.id, text={{modelVariable}}.{{field}})# - {{else}} - #{{modelVariable}}.{{field}}# - {{/if}} - -
- #linkTo(route="{{showRoute}}", key={{modelVariable}}.id, text="View", class="btn btn-outline-primary btn-sm")# - #linkTo(route="{{editRoute}}", key={{modelVariable}}.id, text="Edit", class="btn btn-outline-secondary btn-sm")# - {{#if hasDelete}} - #linkTo(route="{{deleteRoute}}", key={{modelVariable}}.id, method="delete", confirm="Are you sure?", text="Delete", class="btn btn-outline-danger btn-sm")# - {{/if}} -
-
-
- {{else}} - -
- - -
-
- {{#if hasImage}} -
- -
- {{/if}} -
-
- #linkTo(route="{{showRoute}}", key={{modelVariable}}.id, text={{modelVariable}}.{{titleField}})# -
- {{#cardFields}} -

- {{label}}: #{{modelVariable}}.{{field}}# -

- {{/cardFields}} -
- #linkTo(route="{{showRoute}}", key={{modelVariable}}.id, text="View", class="btn btn-primary btn-sm")# - #linkTo(route="{{editRoute}}", key={{modelVariable}}.id, text="Edit", class="btn btn-secondary btn-sm")# -
-
-
-
-
-
- {{/if}} - - {{#if hasPagination}} - -
- #paginationLinks({{modelVariable}})# -
- {{/if}} - - -
-

No {{pluralDisplayName}} found

-

{{emptyStateMessage}}

- {{#if hasCreateButton}} - #linkTo(route="new{{modelName}}", text="Create {{singularDisplayName}}", class="btn btn-primary")# - {{/if}} -
-
-
-
-
-
-``` - -## Error Recovery Framework - -### Error Detection Patterns -```javascript -class ErrorDetector { - detectErrors(generatedCode, componentType) { - const errors = []; - - // Mixed argument detection - const mixedArgErrors = this.detectMixedArguments(generatedCode); - if (mixedArgErrors.length > 0) { - errors.push({ - type: 'mixed_arguments', - severity: 'critical', - locations: mixedArgErrors, - description: 'Mixed positional and named arguments detected', - recovery: 'convert_to_consistent_style' - }); - } - - // Query/Array confusion detection - const queryArrayErrors = this.detectQueryArrayConfusion(generatedCode); - if (queryArrayErrors.length > 0) { - errors.push({ - type: 'query_array_confusion', - severity: 'critical', - locations: queryArrayErrors, - description: 'ArrayLen() or array loops used on query objects', - recovery: 'use_proper_query_methods' - }); - } - - // Naming convention errors - const namingErrors = this.detectNamingErrors(generatedCode, componentType); - if (namingErrors.length > 0) { - errors.push({ - type: 'naming_convention', - severity: 'medium', - locations: namingErrors, - description: 'Incorrect naming conventions detected', - recovery: 'fix_naming_conventions' - }); - } - - return errors; - } - - detectMixedArguments(code) { - const mixedArgPatterns = [ - /hasMany\s*\(\s*"[^"]*"\s*,\s*\w+\s*=/g, - /belongsTo\s*\(\s*"[^"]*"\s*,\s*\w+\s*=/g, - /findByKey\s*\(\s*[^,]*,\s*\w+\s*=/g, - /validatesPresenceOf\s*\(\s*"[^"]*"\s*,\s*\w+\s*=/g - ]; - - const errors = []; - mixedArgPatterns.forEach(pattern => { - const matches = [...code.matchAll(pattern)]; - matches.forEach(match => { - errors.push({ - line: this.getLineNumber(code, match.index), - column: match.index, - text: match[0], - suggestion: this.suggestConsistentArgs(match[0]) - }); - }); - }); - - return errors; - } - - detectQueryArrayConfusion(code) { - const queryArrayPatterns = [ - /ArrayLen\s*\(\s*\w+\s*\.\s*\w+\s*\(\s*\)\s*\)/g, - / { - const matches = [...code.matchAll(pattern)]; - matches.forEach(match => { - errors.push({ - line: this.getLineNumber(code, match.index), - column: match.index, - text: match[0], - suggestion: this.suggestQueryMethod(match[0]) - }); - }); - }); - - return errors; - } -} -``` - -### Recovery Action System -```javascript -class ErrorRecoverySystem { - recoverFromError(error, originalCode, context) { - switch (error.type) { - case 'mixed_arguments': - return this.fixMixedArguments(originalCode, error, context); - case 'query_array_confusion': - return this.fixQueryArrayConfusion(originalCode, error, context); - case 'naming_convention': - return this.fixNamingConventions(originalCode, error, context); - case 'validation_failure': - return this.fixValidationFailure(originalCode, error, context); - default: - return this.genericErrorRecovery(originalCode, error, context); - } - } - - fixMixedArguments(code, error, context) { - // Determine dominant argument style in existing codebase - const argumentStyle = this.detectDominantArgumentStyle(context.existingCode); - - // Convert all function calls to consistent style - let fixedCode = code; - - if (argumentStyle === 'named') { - // Convert to all named arguments - fixedCode = fixedCode.replace( - /hasMany\s*\(\s*"([^"]*)"\s*,\s*(\w+)\s*=\s*"([^"]*)"/g, - 'hasMany(name="$1", $2="$3")' - ); - fixedCode = fixedCode.replace( - /belongsTo\s*\(\s*"([^"]*)"\s*,\s*(\w+)\s*=\s*"([^"]*)"/g, - 'belongsTo(name="$1", $2="$3")' - ); - } else { - // Convert to all positional arguments - fixedCode = fixedCode.replace( - /hasMany\s*\(\s*name\s*=\s*"([^"]*)"\s*,\s*\w+\s*=\s*"[^"]*"/g, - 'hasMany("$1")' - ); - fixedCode = fixedCode.replace( - /belongsTo\s*\(\s*name\s*=\s*"([^"]*)"\s*,\s*\w+\s*=\s*"[^"]*"/g, - 'belongsTo("$1")' - ); - } - - return { - fixedCode: fixedCode, - changes: this.getChanges(code, fixedCode), - validationRequired: true - }; - } - - fixQueryArrayConfusion(code, error, context) { - let fixedCode = code; - - // Fix ArrayLen() on queries - fixedCode = fixedCode.replace( - /ArrayLen\s*\(\s*(\w+)\s*\.\s*(\w+)\s*\(\s*\)\s*\)/g, - '$1.$2().recordCount' - ); - - // Fix array loops on queries - fixedCode = fixedCode.replace( - //g, - '' - ); - - // Fix for-in loops on queries - fixedCode = fixedCode.replace( - /for\s*\(\s*(\w+)\s+in\s+(\w+)\.(\w+)\(\)\s*\)/g, - '/* Use instead of for-in loop */' - ); - - return { - fixedCode: fixedCode, - changes: this.getChanges(code, fixedCode), - validationRequired: true - }; - } - - fixNamingConventions(code, error, context) { - let fixedCode = code; - - // Fix model naming (should be singular) - const modelNameFixes = { - 'Users.cfc': 'User.cfc', - 'Posts.cfc': 'Post.cfc', - 'Comments.cfc': 'Comment.cfc', - 'Products.cfc': 'Product.cfc' - }; - - // Fix controller naming (should be plural) - const controllerNameFixes = { - 'UserController.cfc': 'UsersController.cfc', - 'PostController.cfc': 'PostsController.cfc', - 'CommentController.cfc': 'CommentsController.cfc' - }; - - // Apply naming fixes - Object.entries(modelNameFixes).forEach(([wrong, correct]) => { - if (code.includes(wrong)) { - fixedCode = fixedCode.replace(new RegExp(wrong, 'g'), correct); - } - }); - - Object.entries(controllerNameFixes).forEach(([wrong, correct]) => { - if (code.includes(wrong)) { - fixedCode = fixedCode.replace(new RegExp(wrong, 'g'), correct); - } - }); - - return { - fixedCode: fixedCode, - changes: this.getChanges(code, fixedCode), - validationRequired: true - }; - } -} -``` - -### Progressive Recovery Strategy -```javascript -class ProgressiveRecoveryStrategy { - attemptRecovery(error, originalCode, context, attemptNumber = 1) { - const maxAttempts = 3; - - if (attemptNumber > maxAttempts) { - return { - success: false, - reason: 'Maximum recovery attempts exceeded', - fallbackAction: 'request_human_intervention' - }; - } - - // Progressive recovery strategies - const strategies = [ - 'template_substitution', // Attempt 1: Try different template - 'pattern_simplification', // Attempt 2: Simplify the pattern - 'manual_intervention' // Attempt 3: Request human help - ]; - - const strategy = strategies[attemptNumber - 1]; - - switch (strategy) { - case 'template_substitution': - return this.tryAlternativeTemplate(error, originalCode, context); - case 'pattern_simplification': - return this.simplifyPattern(error, originalCode, context); - case 'manual_intervention': - return this.requestManualIntervention(error, originalCode, context); - } - } - - tryAlternativeTemplate(error, originalCode, context) { - // Load alternative template from .ai documentation - const alternativeTemplate = this.loadAlternativeTemplate(context.componentType, error.type); - - if (alternativeTemplate) { - const regeneratedCode = this.generateFromTemplate(alternativeTemplate, context.data); - const errors = this.validateCode(regeneratedCode); - - if (errors.length === 0) { - return { - success: true, - fixedCode: regeneratedCode, - strategy: 'alternative_template', - template: alternativeTemplate.name - }; - } - } - - return { success: false, reason: 'No suitable alternative template found' }; - } - - simplifyPattern(error, originalCode, context) { - // Remove complex features and use basic patterns - const simplifiedContext = this.simplifyContext(context); - const basicTemplate = this.loadBasicTemplate(context.componentType); - - const simplifiedCode = this.generateFromTemplate(basicTemplate, simplifiedContext); - const errors = this.validateCode(simplifiedCode); - - if (errors.length === 0) { - return { - success: true, - fixedCode: simplifiedCode, - strategy: 'simplified_pattern', - removedFeatures: this.getRemovedFeatures(context, simplifiedContext) - }; - } - - return { success: false, reason: 'Simplified pattern still contains errors' }; - } -} -``` - -## Quality Assurance Integration - -### Continuous Validation System -```javascript -class ContinuousValidator { - validateDuringGeneration(code, phase, context) { - const validations = { - syntax: this.validateSyntax(code), - patterns: this.validatePatterns(code, context), - security: this.validateSecurity(code, context), - performance: this.validatePerformance(code, context), - consistency: this.validateConsistency(code, context) - }; - - const errors = []; - const warnings = []; - - Object.entries(validations).forEach(([type, results]) => { - errors.push(...results.errors); - warnings.push(...results.warnings); - }); - - return { - valid: errors.length === 0, - errors: errors, - warnings: warnings, - phase: phase, - timestamp: new Date().toISOString() - }; - } - - validatePatterns(code, context) { - const errors = []; - const warnings = []; - - // Check against known anti-patterns from .ai documentation - const antiPatterns = this.loadAntiPatterns(); - - antiPatterns.forEach(pattern => { - if (this.matchesAntiPattern(code, pattern)) { - errors.push({ - type: 'anti_pattern', - pattern: pattern.name, - description: pattern.description, - suggestion: pattern.solution - }); - } - }); - - // Check consistency with existing codebase - const consistencyIssues = this.checkConsistency(code, context.existingCode); - warnings.push(...consistencyIssues); - - return { errors, warnings }; - } - - validateSecurity(code, context) { - const errors = []; - const warnings = []; - - // Check for CSRF protection in forms - if (code.includes(' { - if (pattern.test(code)) { - errors.push({ - type: 'security', - issue: 'potential_sql_injection', - description: 'Direct variable interpolation in SQL may be vulnerable to injection' - }); - } - }); - - return { errors, warnings }; - } -} -``` - -This template-driven implementation system ensures consistent, high-quality code generation with comprehensive error recovery and continuous quality validation, making the `/wheels_execute` workflow robust and reliable. \ No newline at end of file diff --git a/.claude/commands/wheels_build.md b/.claude/commands/wheels_build.md new file mode 100644 index 0000000000..d4af2b08a3 --- /dev/null +++ b/.claude/commands/wheels_build.md @@ -0,0 +1,370 @@ +# /wheels_build - Implement From Specification + +## Description +Implement a Wheels application feature from an approved specification. Builds components in the correct order with incremental testing after each step. + +## Usage +``` +/wheels_build # Build from latest spec (.specs/current.md) +/wheels_build [spec-filename] # Build from a specific spec file +``` + +## Examples +``` +/wheels_build +/wheels_build 20250930-163000-blog-posts-comments.md +``` + +## Prerequisites + +- An approved spec must exist in `.specs/` (generated by `/wheels_spec`) +- If no spec exists, tell the user to run `/wheels_spec` first + +## Workflow + +### Step 0: Load the Spec + +Read the approved specification: +``` +Read(".specs/current.md") +``` +Or if a specific filename was given: +``` +Read(".specs/[filename]") +``` + +Verify the spec has `**Status:** approved`. If status is `completed`, warn the user this spec was already built and ask if they want to rebuild. + +Update the spec status: +``` +Edit: **Status:** approved -> **Status:** in-progress +``` + +### Step 1: Detect Available Tools + +Check if MCP tools are available: +``` +ls .mcp.json +``` + +If `.mcp.json` exists: +- Use `mcp__wheels__wheels_generate()` for code generation +- Use `mcp__wheels__wheels_migrate()` for migrations +- Use `mcp__wheels__wheels_test()` for tests +- Use `mcp__wheels__wheels_reload()` for app reload +- Use `mcp__wheels__wheels_server(action="status")` to get the server port + +If `.mcp.json` does not exist: +- Use CLI commands (`wheels g model`, `wheels dbmigrate`, `wheels test run`) +- Read `server.json` for port, default to 8080 + +Store the base URL (e.g., `http://localhost:PORT`) for testing throughout. + +### Step 2: Create Task List + +Break the spec into granular tasks using TaskCreate. Follow this exact build order: + +1. **Migrations** (database tables must exist before models reference them) +2. **Models** (models must exist before controllers use them) +3. **Controllers** (controllers must exist before views reference actions) +4. **Views** (layout first, then individual views) +5. **Routes** (configure after controllers/views exist) +6. **Tests** (verify everything works) + +Create one task per: +- Migration file +- Model file +- Controller file +- View file (each view is its own task) +- Route configuration change +- Test suite + +### Step 3: Build Migrations + +For each migration in the spec: + +1. Mark the task as `in_progress` +2. Generate the migration file + +**If using MCP:** +``` +mcp__wheels__wheels_generate(type="migration", name="CreatePostsTable", attributes="...") +``` + +**If using CLI:** +``` +wheels g migration CreatePostsTable +``` + +3. After generation, read the migration file and fix known issues: + +**Fix string boolean values** (CLI generators produce these): +```cfm +BAD: createTable(name='posts', force='false', id='true') +GOOD: createTable(name='posts') +``` + +**Fix database-specific SQL in seed data**: +```cfm +BAD: DATE_SUB(NOW(), INTERVAL 1 DAY) +GOOD: TIMESTAMP '#DateFormat(DateAdd("d", -1, Now()), "yyyy-mm-dd")# #TimeFormat(DateAdd("d", -1, Now()), "HH:mm:ss")#' +``` + +4. Run the migration: +``` +mcp__wheels__wheels_migrate(action="latest") # MCP +wheels dbmigrate latest # CLI +``` + +5. Verify migrations ran successfully (check output for errors) +6. Mark the task as `completed` + +### Step 4: Build Models + +For each model in the spec: + +1. Mark the task as `in_progress` +2. Generate the model: +``` +mcp__wheels__wheels_generate(type="model", name="Post", attributes="title:string,content:text") +``` +3. Read the generated file and enhance it with the spec's requirements: + - Add associations with **all named parameters** (never mix positional and named) + - Add validations + - Add callbacks + - Add custom methods + +**Critical: Use the wheels-codegen skill patterns.** Key rules: +```cfm +// ALWAYS use all-named parameters when adding extra options: +hasMany(name="comments", dependent="delete") // Correct +belongsTo(name="post") // Correct +hasMany("comments") // Correct (all positional, no extras) + +// NEVER mix positional and named: +hasMany("comments", dependent="delete") // WRONG +``` + +4. Reload the app and verify no errors: +```bash +curl -s http://localhost:PORT/?reload=true -I # Should return 200 +``` +5. Mark the task as `completed` + +### Step 5: Build Controllers + +For each controller in the spec: + +1. Mark the task as `in_progress` +2. Generate the controller: +``` +mcp__wheels__wheels_generate(type="controller", name="Posts", actions="index,show,new,create,edit,update,delete") +``` +3. Read the generated file and enhance per the spec: + - Add `config()` with filters and verifies + - Implement each action per the spec + - Add private filter functions (filters MUST be private) + - Add flash messages for create/update/delete + - Handle validation failures (renderView back to form) + +**Key controller patterns:** +```cfm +// Filter functions MUST be private +private function findPost() { + post = model("Post").findByKey(key=params.key); + if (!isObject(post)) { + flashInsert(error="Post not found."); + redirectTo(action="index"); + } +} + +// Create action pattern +function create() { + post = model("Post").new(params.post); + if (post.save()) { + flashInsert(success="Post created!"); + redirectTo(action="show", key=post.id); + } else { + flashInsert(error="Please fix the errors below."); + renderView(action="new"); + } +} +``` + +4. Reload and test that controller actions respond: +```bash +curl -s http://localhost:PORT/posts -I # Should return 200 or redirect +``` +5. Mark the task as `completed` + +### Step 6: Build Views + +Build views in this order: layout first, then index, show, new, edit. + +**Layout (if needed):** +1. Create `app/views/layout.cfm` with the spec's frontend stack +2. Include: `csrfMetaTags()`, `styleSheetLinkTag()`, `flashMessages()`, `includeContent()`, `javaScriptIncludeTag()` +3. Test: reload and hit homepage, verify no 500 error + +**For each view:** + +1. Mark the task as `in_progress` +2. Create the view file per the spec +3. Every view MUST start with `cfparam` declarations for its data dependencies: +```cfm + + +``` + +4. **Critical view patterns to follow:** + +**Index views - query loops:** +```cfm + +

#posts.title#

+
+``` + +**Show views - association access:** +```cfm + + + + +

#comments.content#

+
+``` + +**Form views - validation errors:** +```cfm +#textField(objectName="post", property="title", label=false)# + +

#post.allErrors("title")[1].message#

+
+``` + +**Form views - CSRF protection:** +```cfm +#startFormTag(controller="posts", action="create", method="post")# + + + #submitTag(value="Save")# +#endFormTag()# +``` + +5. Test IMMEDIATELY after creating each view: +```bash +# Check HTTP status +curl -s http://localhost:PORT/posts -I # 200? +# Check content renders +curl -s http://localhost:PORT/posts | grep "expected text" +``` + +6. If the view returns 500, read the error output and fix before moving on +7. Mark the task as `completed` + +**Key rule: Never start the next view until the current one works.** + +### Step 7: Configure Routes + +1. Mark the task as `in_progress` +2. Read `config/routes.cfm` +3. Add/modify routes per the spec + +**Route ordering matters:** +```cfm +mapper() + // 1. Resource routes first + .resources("posts") + .resources("comments") + + // 2. Root route + .root(to="posts##index", method="get") + + // 3. Wildcard last + .wildcard() +.end(); +``` + +4. Reload the app +5. Test that URLs resolve: +```bash +curl -s http://localhost:PORT/ -I # Root +curl -s http://localhost:PORT/posts -I # Index +curl -s http://localhost:PORT/posts/1 -I # Show +curl -s http://localhost:PORT/posts/new -I # New form +``` +6. Mark the task as `completed` + +### Step 8: Run Tests + +1. Mark the task as `in_progress` +2. Run the test suite: +``` +mcp__wheels__wheels_test() # MCP +wheels test run # CLI +``` +3. If tests fail, read failures and fix the issues +4. Re-run until all tests pass +5. Mark the task as `completed` + +### Step 9: Final Verification + +Do a quick end-to-end check: +- Hit each major URL and verify 200 status +- Verify content appears (not just status code) +- Check that the frontend stack loads (CSS/JS) +- Verify forms have fields, labels, and submit buttons + +### Step 10: Update the Spec + +Mark the spec as completed: +``` +Edit: **Status:** in-progress -> **Status:** completed +``` + +Add a "Files Created" section listing all files that were created or modified. + +### Step 11: Report Results + +Present a summary: +- What was built (list of components) +- Test results (pass/fail) +- URLs to visit +- Files created/modified +- Any issues encountered and how they were resolved + +Tell the user: +``` +Implementation complete. Run /wheels_validate for a thorough verification. +``` + +## Error Recovery + +If a step fails: + +1. **Do not skip it.** Fix the issue before moving to the next step. +2. **Read error output carefully.** Wheels error pages contain useful details. +3. **Check for common causes:** + - Mixed argument styles in model associations + - Missing `cfparam` in views + - Forgotten `private` keyword on filter functions + - String boolean values in migrations + - Database-specific SQL in seed data +4. **If stuck after 2 attempts**, report what failed and ask the user for guidance. + +## Build Without a Spec + +If the user runs `/wheels_build` with no spec file: +1. Check `.specs/current.md` - if it exists and is approved, use it +2. If no spec exists, tell the user: +``` +No approved specification found. Run /wheels_spec first to create one, +or describe what you want to build and I will create a spec for you. +``` + +## Integration with Other Commands + +- **Input**: Reads `.specs/*.md` files generated by `/wheels_spec` +- **Uses**: Skills for code generation patterns (wheels-codegen, wheels-testing, wheels-scaffold) +- **Output**: A working implementation ready for `/wheels_validate` diff --git a/.claude/commands/wheels_execute.md b/.claude/commands/wheels_execute.md deleted file mode 100755 index 0a23cbbc68..0000000000 --- a/.claude/commands/wheels_execute.md +++ /dev/null @@ -1,2312 +0,0 @@ -# /wheels_execute - Comprehensive Wheels Development Workflow - -## Description -Execute a complete, systematic Wheels development workflow that implements features with professional quality, comprehensive testing, and bulletproof error prevention. - -## 🎓 Key Learnings Implemented - -This workflow incorporates critical lessons from real-world Wheels development: - -### 1. **Views Are Critical** - Don't Skip Them -- Models and controllers are quick to generate, but **views are what make the application functional** -- All CRUD views (index, show, new, edit) must be created for a working application -- Forms need validation error displays, not just input fields -- **Views are where most errors occur** - query access patterns, association handling - -### 2. **Test Incrementally, Not At The End** -- Generate model → Test it works → Then move to controller -- Generate controller → Test actions return 200 → Then create views -- Generate view → Test it renders → Then move to next view -- **Don't build everything then test** - you'll waste time debugging stacked errors - -### 3. **HTTP 200 ≠ Success** - Verify Content Too -- A page can return 200 OK but contain error messages in the HTML -- Always check: `curl URL | grep "Expected Content"` not just `curl URL -I` -- Verify records display, forms render, links point to correct URLs - -### 4. **Query Access Inside Loops Requires Special Handling** -```cfm -❌ BAD: #post.comments.recordCount# (property access - fails) -✅ GOOD: - #postComments.recordCount# (method call - works) -``` - -### 5. **Migration Date Functions Are Database-Specific** -```cfm -❌ BAD: DATE_SUB(NOW(), INTERVAL 1 DAY) (MySQL only) -✅ GOOD: var day1 = DateAdd("d", -1, Now()) - TIMESTAMP '#DateFormat(day1, "yyyy-mm-dd")# #TimeFormat(day1, "HH:mm:ss")#' -``` - -### 6. **Forms Without Error Display Are Incomplete** -Every form field needs: -```cfm -#textField(objectName="resource", property="name", label=false)# - -

#resource.allErrors("name")[1]#

-
-``` - -### 7. **Controller Filters Must Be Private** -```cfm -private function findResource() { // Must be private - resource = model("Resource").findByKey(key=params.key); - if (!isObject(resource)) { - flashInsert(error="Resource not found."); - redirectTo(action="index"); - } -} -``` - -### 8. **Use Consistent Argument Styles** -```cfm -❌ MIXED: hasMany("comments", dependent="delete") -✅ CONSISTENT: hasMany(name="comments", dependent="delete") -``` - -### 9. **Frontend Stack Integration Via Layout Templates** -- Provide pre-built layouts (Basic, Tailwind+Alpine+HTMX, Bootstrap) -- Include CDN links, navigation structure, flash messages -- Users choose template during generation - -### 10. **Resource Routes Work Differently Than Rails** -- `.resources("posts")` generates `/posts/:id` (not `/posts/show/:id`) -- Test actual generated URLs: `curl URL | grep 'href="'` - -## Usage -``` -/wheels_execute [task_description] -``` - -## Examples -``` -/wheels_execute create a blog with posts and comments -/wheels_execute add user authentication to the application -/wheels_execute build an e-commerce product catalog with shopping cart -/wheels_execute create admin dashboard for user management -/wheels_execute implement contact form with email notifications -``` - -## Workflow Overview - -The `/wheels_execute` command implements a **Spec-Driven Development** workflow with incremental task-based implementation: - -### Execution Modes: - -#### **Mode 1: Interactive Spec-Driven (Recommended)** -User gets to review and approve the implementation plan before any code is written. - -0. **MCP Detection & Documentation Loading** - Load patterns and verify tools -1. **Requirements Analysis** - Parse user request and identify components needed -2. **📋 Specification Generation** - Create detailed spec with all components, views, routes -3. **✋ User Approval Checkpoint** - Present spec and task list, wait for approval -4. **📝 Task List Creation** - Break spec into granular, testable tasks using TodoWrite -5. **🔄 Incremental Implementation** - Implement one task at a time with testing -6. **✅ Task Completion Tracking** - Mark tasks complete only after testing passes -7. **🎯 Final Verification** - Comprehensive browser testing of complete feature -8. **📊 Results Report** - Summary of what was built with evidence - -#### **Mode 2: Autonomous (Fast)** -Claude Code implements immediately without approval (use with caution). - -Same as Mode 1 but skips Step 3 (User Approval Checkpoint). - -### Why Spec-Driven Development? - -**Traditional Approach Problems:** -- User doesn't know what will be built until it's done -- Changes mid-implementation waste time -- Missing requirements discovered at the end -- No visibility into progress - -**Spec-Driven Approach Benefits:** -- ✅ User sees complete plan before coding starts -- ✅ User can request changes to the spec -- ✅ Clear task list shows progress in real-time -- ✅ Each task is tested before moving forward -- ✅ User knows exactly what they'll get - -## Detailed Phase Descriptions - -### Phase 0: MCP Detection & Documentation Loading (1-2 minutes) -**Purpose:** Verify tools are available and load relevant patterns before planning. - -**Steps:** -1. Check for `.mcp.json` - if exists, MCP is mandatory -2. Test MCP connection: `mcp__wheels__wheels_server(action="status")` -3. Get server port for testing URLs later -4. Load critical documentation: - - `.ai/wheels/troubleshooting/common-errors.md` - - `.ai/wheels/patterns/validation-templates.md` - - Task-specific docs based on keywords (blog, auth, API, etc.) - -**Output:** Confirmation that tools are ready and patterns are loaded. - ---- - -### Phase 1: Requirements Analysis (2-3 minutes) -**Purpose:** Parse user request, load previous specs, and identify what needs to be built. - -**Step 1: Load Previous Specifications** -```javascript -// Check if .specs/ directory exists -Glob(pattern=".specs/*.md") - -// If specs exist, read recent ones to understand what's already built -Read(".specs/current.md") // Current state of application - -// Load last 3 specs to understand project evolution -Read(".specs/20250930-163000-blog-posts-comments.md") -Read(".specs/20250930-170000-add-user-authentication.md") -``` - -**Step 2: Analyze Current Request** -- Extract entities (User, Post, Comment, Product, etc.) -- Identify relationships (Post hasMany Comments) -- Determine CRUD requirements (which resources need full CRUD vs partial) -- Detect frontend requirements (Tailwind, Alpine.js, HTMX, Bootstrap, etc.) -- Identify special features (authentication, file upload, email, API endpoints) - -**Step 3: Determine What's New vs What Exists** -``` -Previous Specs Show: -- Post and Comment models already exist -- Tailwind CSS layout already implemented - -Current Request: "add tags to posts" - -Analysis: -- NEW: Tag model -- NEW: PostTag join table -- MODIFY: Post model (add hasManyThrough relationship) -- NEW: Tags controller (CRUD) -- NEW: Tag views (index, show) -- MODIFY: posts/new.cfm and posts/edit.cfm (add tag selection) -``` - -**Example:** -``` -User Request: "create a blog with posts and comments, use Tailwind CSS" - -Previous Specs: None (fresh installation) - -Analysis: -- Entities: Post, Comment -- Relationships: Post hasMany Comments (dependent delete) -- CRUD: Posts (full CRUD), Comments (create, delete only) -- Frontend: Tailwind CSS + Alpine.js (for interactive elements) -- Special: Need comment form on post show page -- Status: Complete new feature (not building on existing) -``` - -**Output:** Structured analysis showing what's new, what's modified, and what already exists. - ---- - -### Phase 2: 📋 Specification Generation with Versioning (3-5 minutes) -**Purpose:** Create a detailed, human-readable specification document and save it for future reference. - -**Specification Storage:** - -All specifications are saved in `.specs/` directory: -``` -.specs/ -├── 20250930-163000-blog-posts-comments.md (Initial blog) -├── 20250930-170000-add-user-authentication.md (Added auth) -├── 20250930-173000-add-tags-to-posts.md (Added tags) -└── current.md (Symlink to latest) -``` - -**Filename Format:** `YYYYMMDD-HHMMSS-feature-description.md` - -**Spec File Structure:** -```markdown -# Feature Specification: Blog with Posts and Comments - -**Created:** 2025-09-30 16:30:00 -**Status:** approved | in-progress | completed | modified -**Estimated Time:** 20-30 minutes -**Actual Time:** [filled in upon completion] - -## User Request -"create a blog with posts and comments, use Tailwind CSS and Alpine.js" - -## Previous Specs -- None (initial implementation) - -## This Spec Builds On -- Fresh Wheels installation - -## Components to Add -- Post model -- Comment model -- Posts controller -- Comments controller -- Views for posts (index, show, new, edit) -- Tailwind + Alpine.js layout - -[Full specification details follow...] -``` - -**Benefits of Spec Versioning:** -1. **Audit Trail** - Complete history of what was built when -2. **Incremental Development** - Each new feature references previous specs -3. **Rollback Capability** - Can see what changed between versions -4. **Team Communication** - Share specs with team members -5. **Documentation** - Automatic project documentation -6. **Context Awareness** - Claude knows what already exists before planning new features - -**Specification Includes:** - -#### **1. Database Schema** -``` -Posts Table: -- id (primary key) -- title (string, required, 3-200 chars) -- slug (string, unique, auto-generated from title) -- content (text, required, min 10 chars) -- published (boolean, default false) -- publishedAt (datetime, nullable) -- createdAt, updatedAt (timestamps) - -Indexes: -- slug (unique) -- published + publishedAt (composite for queries) - -Comments Table: -- id (primary key) -- content (text, required, 3-1000 chars) -- authorName (string, required, 2-100 chars) -- authorEmail (string, required, valid email) -- postId (foreign key to posts, on delete cascade) -- createdAt, updatedAt (timestamps) - -Indexes: -- postId -- createdAt -``` - -#### **2. Models** -``` -Post Model: -- Associations: hasMany(name="comments", dependent="delete") -- Validations: - - validatesPresenceOf("title,content") - - validatesUniquenessOf(property="slug") - - validatesLengthOf(property="title", minimum=3, maximum=200) - - validatesLengthOf(property="content", minimum=10) -- Methods: - - generateSlug(text) - creates URL-friendly slug - - excerpt(length=200) - returns truncated content - - setSlugAndPublishDate() - callback before validation -- Callbacks: - - beforeValidationOnCreate("setSlugAndPublishDate") - -Comment Model: -- Associations: belongsTo(name="post") -- Validations: - - validatesPresenceOf("content,authorName,authorEmail,postId") - - validatesFormatOf(property="authorEmail", regEx="...") - - validatesLengthOf(property="content", minimum=3, maximum=1000) -- Methods: - - getGravatarUrl(size=80) - returns Gravatar image URL -``` - -#### **3. Controllers** -``` -Posts Controller: -- Actions: index, show, new, create, edit, update, delete -- Filters: findPost (runs for show, edit, update, delete) -- Parameter Verification: key must be integer for show/edit/update/delete -- Flash Messages: Success/error messages for all actions - -Comments Controller: -- Actions: create, delete -- Parameter Verification: postId required for all actions -- Flash Messages: Success/error messages -- Redirects: Always back to post show page -``` - -#### **4. Views (Complete List)** -``` -Layout (layout.cfm): -- Tailwind CSS via CDN -- Alpine.js for interactive elements -- Navigation with links to: Home, Write Post -- Flash messages display area -- Mobile-responsive navigation with hamburger menu - -Posts Views: -- index.cfm: Grid of post cards, show title/excerpt/date/comment count -- show.cfm: Full post with comments section, add comment form (Alpine.js toggle) -- new.cfm: Form with title, slug, content, published checkbox -- edit.cfm: Same as new but pre-populated with post data - -All forms include: -- Field labels -- Validation error displays -- CSRF tokens -- Submit and Cancel buttons -``` - -#### **5. Routes** -``` -Root: / → posts#index -Resources: posts (generates RESTful routes) -Resources: comments (generates RESTful routes) -Wildcard: Enabled for flexibility -``` - -#### **6. Frontend Stack** -``` -- Tailwind CSS: Utility-first styling -- Alpine.js: Reactive components (comment form toggle, mobile menu) -- HTMX: Available for future enhancements -- Google Fonts (Inter): Typography -``` - -#### **7. Sample Data** -``` -10 tech blog posts with: -- Varied titles (HTMX, Tailwind, Security, Testing, etc.) -- Rich HTML content -- Published status -- Staggered publish dates -``` - -**Output:** Complete specification document formatted as markdown. - ---- - -### Phase 3: ✋ User Approval Checkpoint -**Purpose:** Let user review, request changes, or approve the specification. - -**Claude Code Presents:** -```markdown -## 📋 Implementation Specification - -I've analyzed your request and created the following specification: - -[Complete spec from Phase 2] - -## 📝 Implementation Tasks - -If approved, I will implement the following tasks in order: - -1. ✅ Generate Post model with validations and associations -2. ✅ Generate Comment model with validations and associations -3. ✅ Create database migrations (posts, comments, seed data) -4. ✅ Run migrations and verify tables created -5. ✅ Generate Posts controller with all CRUD actions -6. ✅ Generate Comments controller (create, delete) -7. ✅ Create layout with Tailwind CSS, Alpine.js, HTMX -8. ✅ Create posts/index.cfm view and test -9. ✅ Create posts/show.cfm view with comments and test -10. ✅ Create posts/new.cfm form and test -11. ✅ Create posts/edit.cfm form and test -12. ✅ Configure routes (root, resources) -13. ✅ Test complete CRUD workflow -14. ✅ Test comment creation and deletion -15. ✅ Final browser testing of all features - -Estimated time: 20-30 minutes - ---- - -**Please review and respond:** -- Type "approve" to begin implementation -- Type "change: [description]" to request modifications -- Ask questions about any part of the spec -``` - -**User Can:** -- ✅ Approve and proceed -- ✅ Request changes (add/remove features, change frontend stack, etc.) -- ✅ Ask clarifying questions -- ✅ Cancel if not what they wanted - -**Upon Approval:** -1. Create `.specs/` directory if it doesn't exist -2. Generate timestamped filename: `YYYYMMDD-HHMMSS-feature-name.md` -3. Write complete spec to file with metadata: - ```markdown - # Feature Specification: Blog with Posts and Comments - - **Created:** 2025-09-30 16:30:00 - **Status:** approved - **Estimated Time:** 20-30 minutes - **Actual Time:** [to be filled upon completion] - - ## User Request - "create a blog with posts and comments, use Tailwind CSS and Alpine.js" - - ## Previous Specs - - None (initial implementation) - - ## This Spec Builds On - - Fresh Wheels installation - - [Complete specification content...] - ``` -4. Create/update `current.md` symlink pointing to this spec -5. Update spec status to "in-progress" -6. Begin implementation - ---- - -### Phase 4: 📝 Task List Creation with TodoWrite -**Purpose:** Create trackable, granular tasks that show real-time progress. - -**Step 1: Read Current Spec** -```javascript -// Load the spec that was just approved -Read(".specs/current.md") - -// Extract task list from specification -``` - -**Step 2: Create TodoWrite Tasks** -```javascript -TodoWrite({ - todos: [ - { - content: "Generate Post model with validations", - activeForm: "Generating Post model with validations", - status: "pending" - }, - { - content: "Generate Comment model with validations", - activeForm: "Generating Comment model with validations", - status: "pending" - }, - // ... all tasks from spec - ] -}); -``` - -**Step 3: Update Spec Status** -```javascript -// Update .specs/current.md with status change -Edit(".specs/current.md", - old_string="**Status:** approved", - new_string="**Status:** in-progress\n**Started:** [timestamp]" -) -``` - -**Task Granularity:** -- One task per model -- One task per migration -- One task per controller -- One task per view (index, show, new, edit) -- One task per test suite -- One task per major testing phase - -**Why This Matters:** -- User sees progress in real-time -- Claude Code stays focused on one task at a time -- Easy to pause and resume later -- Clear audit trail of what was completed -- Spec file always reflects current status - ---- - -### Phase 5: 🔄 Incremental Implementation with Testing -**Purpose:** Implement one task at a time, test it works, then move to next. - -**Pattern for EACH Task:** - -``` -1. Mark task as in_progress in TodoWrite -2. Implement the task (generate code, create file, etc.) -3. TEST IMMEDIATELY: - - Reload application if needed - - Curl the relevant URL - - Verify HTTP status (200 or expected redirect) - - Verify content appears (grep for expected text) -4. If test passes: - - Mark task as completed in TodoWrite - - Move to next task -5. If test fails: - - Keep task as in_progress - - Debug and fix the issue - - Re-test until passes - - Then mark completed and move on -``` - -**Example Task Implementation:** - -```markdown -Task: "Create posts/index.cfm view and test" - -1. TodoWrite: Mark "Create posts/index.cfm" as in_progress - -2. Generate view: - - Create /app/views/posts/index.cfm - - Use proper query loop pattern - - Handle association access correctly - - Include Tailwind CSS classes - -3. Test immediately: - ```bash - curl -s http://localhost:PORT?reload=true # Reload app - curl -s http://localhost:PORT -I # Check status (200?) - curl -s http://localhost:PORT | grep "Latest Tech Posts" # Content appears? - curl -s http://localhost:PORT | grep -c "article class" # Count posts (10?) - ``` - -4. Results: - ✅ 200 OK - ✅ Title appears - ✅ 10 posts displayed - -5. TodoWrite: Mark "Create posts/index.cfm" as completed - -6. Move to next task: "Create posts/show.cfm view and test" -``` - -**Key Principle:** Never start task N+1 until task N is tested and working. - ---- - -### Phase 6: ✅ Task Completion Tracking -**Purpose:** Maintain accurate progress and provide transparency. - -**TodoWrite Updates:** -- Update status in real-time -- Only ONE task should be "in_progress" at a time -- Mark completed IMMEDIATELY after successful test -- If task fails, document the issue and keep as in_progress - -**Spec File Updates:** -After each major milestone (models complete, controllers complete, views complete): -```javascript -// Update spec with progress -Edit(".specs/current.md", - old_string="## Implementation Progress\n\n[Previous content]", - new_string="## Implementation Progress\n\n**Models:** ✅ Complete (Post, Comment)\n**Controllers:** ✅ Complete (Posts, Comments)\n**Views:** 🔄 In Progress (2/4 complete)\n**Tests:** ⏳ Pending" -) -``` - -**User Visibility:** -User sees live progress: -``` -✅ Generate Post model with validations -✅ Generate Comment model with validations -✅ Create database migrations -✅ Run migrations and verify tables -✅ Generate Posts controller with all CRUD actions -🔄 Create posts/index.cfm view and test (In Progress) -⏳ Create posts/show.cfm view and test (Pending) -⏳ Create posts/new.cfm form and test (Pending) -... -``` - -**Files Created Tracking:** -Maintain list of created files in spec: -```markdown -## Files Created - -**Models:** -- [Post.cfc](app/models/Post.cfc) - Blog post model -- [Comment.cfc](app/models/Comment.cfc) - Comment model - -**Controllers:** -- [Posts.cfc](app/controllers/Posts.cfc) - Posts CRUD controller -- [Comments.cfc](app/controllers/Comments.cfc) - Comments controller - -**Views:** -- [layout.cfm](app/views/layout.cfm) - Main layout -- [posts/index.cfm](app/views/posts/index.cfm) - Post list view -- [posts/show.cfm](app/views/posts/show.cfm) - Post detail view -``` - ---- - -### Phase 7: 🎯 Final Verification -**Purpose:** Comprehensive end-to-end testing of complete feature. - -**Full Test Suite:** -1. Homepage displays all posts -2. Click on post → detail page loads -3. Comment form toggles (Alpine.js) -4. Edit button → edit form loads -5. New post button → new form loads -6. Mobile menu works (Alpine.js) -7. All links point to correct URLs -8. Forms have validation error displays -9. CSRF protection present -10. Responsive design works - -**Evidence Collection:** -```bash -# Test results -curl -s http://localhost:PORT -I # Homepage: 200 OK -curl -s http://localhost:PORT/posts/2 -I # Show: 200 OK -curl -s http://localhost:PORT/posts/new -I # New: 200 OK -curl -s http://localhost:PORT/posts/2/edit -I # Edit: 200 OK - -# Content verification -curl -s http://localhost:PORT | grep "Getting Started with HTMX" # ✅ -curl -s http://localhost:PORT | grep -c "article class" # 10 ✅ -curl -s http://localhost:PORT | grep "Tailwind" # ✅ -curl -s http://localhost:PORT | grep "Alpine" # ✅ -``` - ---- - -### Phase 8: 📊 Results Report and Spec Finalization -**Purpose:** Document what was built with evidence and finalize the specification. - -**Step 1: Calculate Implementation Time** -```javascript -// Get start time from spec -var startTime = Read(".specs/current.md") // Extract "Started:" timestamp -var endTime = Now() -var actualTime = DateDiff("n", startTime, endTime) // Minutes -``` - -**Step 2: Update Spec Status to Completed** -```javascript -// Finalize the spec -Edit(".specs/current.md", - old_string="**Status:** in-progress", - new_string="**Status:** completed\n**Completed:** [timestamp]" -) - -// Add actual implementation time -Edit(".specs/current.md", - old_string="**Actual Time:** [to be filled upon completion]", - new_string="**Actual Time:** [calculated] minutes" -) - -// Add test results summary -Edit(".specs/current.md", - old_string="## Test Results\n\n[To be filled]", - new_string="## Test Results\n\n**All Tests Passed:** ✅\n**Browser Tests:** ✅ All pages verified\n**Test Coverage:** 90%+\n**Performance:** Acceptable" -) -``` - -**Step 3: Generate Results Report** -```markdown -## ✅ Implementation Complete! - -### What Was Built: - -**Database:** -- ✅ Posts table with indexes -- ✅ Comments table with foreign keys -- ✅ 10 sample tech blog posts seeded - -**Models:** -- ✅ Post model (validations, associations, methods) -- ✅ Comment model (validations, associations, Gravatar) - -**Controllers:** -- ✅ Posts controller (full CRUD with filters) -- ✅ Comments controller (create, delete) - -**Views:** -- ✅ Layout with Tailwind CSS, Alpine.js, HTMX -- ✅ posts/index.cfm (grid layout) -- ✅ posts/show.cfm (with comments section) -- ✅ posts/new.cfm (form with validation) -- ✅ posts/edit.cfm (form with validation) - -**Routes:** -- ✅ Root route → posts#index -- ✅ RESTful resources for posts and comments - -### Test Results: - -All pages tested and working: -- ✅ http://localhost:58635 (200 OK, 10 posts displayed) -- ✅ http://localhost:58635/posts/2 (200 OK, comments section visible) -- ✅ http://localhost:58635/posts/new (200 OK, form rendered) -- ✅ http://localhost:58635/posts/2/edit (200 OK, form pre-populated) - -Frontend stack verified: -- ✅ Tailwind CSS loaded and styling applied -- ✅ Alpine.js interactive elements working (mobile menu, comment form toggle) -- ✅ HTMX available for future enhancements - -### Implementation Metrics: - -- **Estimated Time:** 20-30 minutes -- **Actual Time:** 22 minutes -- **Tasks Completed:** 15/15 -- **Test Coverage:** 92% -- **Browser Tests:** 100% pass rate - -### What You Can Do Now: - -1. Visit http://localhost:58635 to see your blog -2. Click any post to view details -3. Click "Write Post" to create new posts -4. Edit or delete posts -5. Add comments to posts -6. Test mobile responsiveness (resize browser) - -### Files Created: - -Models: app/models/Post.cfc, app/models/Comment.cfc -Controllers: app/controllers/Posts.cfc, app/controllers/Comments.cfc -Views: app/views/layout.cfm, app/views/posts/*.cfm -Migrations: app/migrator/migrations/*.cfc -Routes: config/routes.cfm (updated) - -### Specification Reference: - -Full implementation details saved to: [.specs/20250930-163000-blog-posts-comments.md](.specs/20250930-163000-blog-posts-comments.md) - -To build additional features on top of this, run: -``` -/wheels_execute [your next feature request] -``` - -The system will automatically load this spec and understand what already exists. -``` - -**Step 4: Preserve Spec History** -```javascript -// The completed spec remains in .specs/ directory -// current.md symlink stays pointed at latest completed spec -// Next /wheels_execute will create a new spec that references this one -``` - ---- - -### Phase 0 (Fallback): MCP Tools Detection & Validation (30 seconds) -- **🔴 CRITICAL**: This phase is MANDATORY and must be executed FIRST -- **Check MCP Availability**: Verify `.mcp.json` exists in project root -- **Test MCP Connection**: Run `mcp__wheels__wheels_server(action="status")` to validate server is running -- **Enforce MCP Usage**: If `.mcp.json` exists, ALL operations MUST use `mcp__wheels__*` tools -- **Strict Prohibition**: NEVER use CLI commands (`wheels g`, `wheels dbmigrate`, etc.) when MCP exists -- **Port Discovery**: Get running server port from MCP status or `server.json` - -**MCP Detection Logic:** -```bash -# Check for MCP configuration -ls .mcp.json - -# If exists → MCP is MANDATORY -# Use: mcp__wheels__wheels_generate, mcp__wheels__wheels_migrate, etc. -# NEVER use: wheels g, wheels dbmigrate, etc. -``` - -**MCP Tools Reference:** -- `mcp__wheels__wheels_generate(type, name, attributes)` - Generate components -- `mcp__wheels__wheels_migrate(action)` - Run migrations (latest, up, down, info) -- `mcp__wheels__wheels_test(type, reporter)` - Execute tests -- `mcp__wheels__wheels_server(action)` - Manage server (status, start, stop) -- `mcp__wheels__wheels_reload()` - Reload application -- `mcp__wheels__wheels_analyze(target, verbose)` - Analyze project - -### Phase 1: Pre-Flight Documentation Loading (2-3 minutes) -- **Critical Error Prevention**: Always load `common-errors.md` and `validation-templates.md` first -- **Smart Documentation Discovery**: Use task-to-documentation mapping decision tree -- **Project Context Loading**: Understand existing codebase patterns and conventions -- **Pattern Recognition**: Detect argument styles and naming conventions already in use - -**Documentation Loading Decision Tree:** -- **Task includes "blog" + "posts"** → Load: `.ai/wheels/database/models/associations.md`, `.ai/wheels/controllers/rendering.md`, `.ai/wheels/views/data-handling.md` -- **Task includes "authentication" or "login"** → Load: `.ai/wheels/database/models/user-authentication.md`, `.ai/wheels/security/csrf-protection.md`, `.ai/wheels/controllers/filters.md` -- **Task includes "API"** → Load: `.ai/wheels/controllers/api-development.md`, `.ai/wheels/views/rendering.md` -- **Task includes "forms"** → Load: `.ai/wheels/views/helpers/forms.md`, `.ai/wheels/security/csrf-protection.md`, `.ai/wheels/models/validations.md` -- **Task includes "admin" or "dashboard"** → Load: `.ai/wheels/controllers/filters.md`, `.ai/wheels/security/`, `.ai/wheels/views/layouts.md` - -### Phase 2: Intelligent Analysis & Planning with View Requirements (3-5 minutes) -- **Requirement Analysis**: Parse natural language into specific Wheels components -- **Component Mapping**: Identify models, controllers, **AND ALL REQUIRED VIEWS**, migrations needed -- **View Requirements Planning**: For each controller action, identify required view: - - `index` action → needs `index.cfm` (list view with proper query loops) - - `show` action → needs `show.cfm` (detail view with association access) - - `new` action → needs `new.cfm` (form with validation error display) - - `edit` action → needs `edit.cfm` (form with pre-populated data) - - Plan forms with: field labels, error displays, CSRF tokens, submit buttons -- **Frontend Stack Selection**: Choose layout template (Basic, Tailwind+Alpine+HTMX, Bootstrap) -- **Dependency Analysis**: Determine implementation order and resolve conflicts -- **Browser Test Planning**: Plan comprehensive user flow testing scenarios -- **Risk Assessment**: Identify potential issues and mitigation strategies - -### Phase 3: Incremental Implementation with Real-Time Testing (10-20 minutes) - -**🚨 CRITICAL: Test each component IMMEDIATELY after generation before moving to next** - -#### Step-by-Step Implementation Order: - -**Step 1: Generate & Test Models** -- Generate model via MCP -- Enhance with validations, associations, methods -- **TEST**: Verify model instantiates: `curl http://localhost:PORT?reload=true` -- **TEST**: Check no errors in model code -- ✅ Only proceed if model works - -**Step 2: Run & Test Migrations** -- Generate migrations for database schema -- **Fix database-specific functions**: Use CFML `DateAdd()` + `TIMESTAMP` formatting, NOT `DATE_SUB()` -- Run migrations via MCP -- **TEST**: Verify tables exist and migrations complete successfully -- ✅ Only proceed if database is ready - -**Step 3: Generate & Test Controllers** -- Generate controller via MCP -- Add actions, filters, parameter verification -- **Use consistent named parameters**: `findByKey(key=params.key, include="assoc")` -- **TEST**: Hit controller action URL: `curl http://localhost:PORT/resource -I` -- **TEST**: Verify returns 200 or expected redirect (not 500 error) -- ✅ Only proceed if controller actions work - -**Step 4: Generate & Test Layout** -- Choose frontend stack template (Tailwind+Alpine+HTMX recommended) -- Create layout.cfm with navigation, flash messages, content area -- Include CDN links for CSS/JS libraries -- **TEST**: Reload app and hit homepage -- **TEST**: Verify layout loads without errors -- ✅ Only proceed if layout renders - -**Step 5: Generate & Test Views (ONE AT A TIME)** - -**For each view:** -1. Create view file (index.cfm, show.cfm, new.cfm, edit.cfm) -2. Use proper query access patterns: - ```cfm - ❌ BAD: #resource.association.recordCount# - ✅ GOOD: - #assocRecords.recordCount# - ``` -3. Include validation error displays in forms: - ```cfm - -

#objectName.allErrors("property")[1]#

-
- ``` -4. **TEST IMMEDIATELY**: `curl http://localhost:PORT/resource -I` (should return 200) -5. **TEST CONTENT**: `curl http://localhost:PORT/resource | grep "Expected Content"` -6. ✅ Only proceed to next view if current view works - -**Step 6: Configure & Test Routes** -- Add resource routes to routes.cfm -- Set root route -- **TEST**: Reload app -- **TEST**: Verify all route URLs work (index, show, new, edit) -- ✅ Only proceed if all routes map correctly - -**Real-Time Anti-Pattern Detection:** -- During code generation, check for anti-patterns BEFORE saving: - ```cfm - // DETECT & FIX: Mixed argument styles - hasMany("comments", dependent="delete"); // ❌ STOP - Fix before proceeding - hasMany(name="comments", dependent="delete"); // ✅ Save and continue - - // DETECT & FIX: Query/Array confusion - // ❌ STOP - Fix before proceeding - // ✅ Save and continue - ``` - -**Error Recovery at Each Step:** -- If any test fails, STOP and fix before proceeding -- Don't generate more code on top of broken code -- Fix the current component until tests pass -- Document what was fixed for learning - -**Anti-Pattern Detection During Implementation:** -```cfm -// DETECT: Mixed argument styles -hasMany("comments", dependent="delete"); // ❌ STOP - Fix before proceeding -hasMany(name="comments", dependent="delete"); // ✅ Continue - -// DETECT: Query/Array confusion - // ❌ STOP - Fix before proceeding - // ✅ Continue - -// DETECT: Missing CSRF protection -#startFormTag(...)# // ❌ STOP - Add CSRF token -#startFormTag(...)##authenticityToken()# // ✅ Continue -``` - -### Phase 4: TestBox BDD Test Suite Creation (10-20 minutes) -- **⏰ TIMING**: Tests are written AFTER implementation is complete (not before - this is not TDD) -- **✅ REQUIREMENT**: Tests MUST be written BEFORE marking feature complete -- **🧹 CLEANUP**: All tests must include proper `beforeEach()` and `afterEach()` for isolation -- **Model Tests**: Write BDD specs for all model functionality, validations, and associations -- **Controller Tests**: Write BDD specs for all controller actions and security filters -- **Integration Tests**: Write BDD specs for complete user workflows and CRUD operations -- **Test Data Setup**: Create fixtures and test data for comprehensive testing -- **Validation Testing**: Write BDD specs for all form validation scenarios -- **Security Testing**: Write BDD specs for authentication, authorization, and CSRF protection - -**Test Writing Order:** -1. Implementation complete → 2. Write tests → 3. Run tests → 4. Mark feature complete - -### Phase 5: Multi-Level Testing Execution (3-8 minutes) -- **Unit Test Execution**: Run all model and controller BDD specs -- **Integration Test Execution**: Run all workflow and CRUD BDD specs -- **Migration Testing**: Verify database changes work correctly -- **Test Coverage Analysis**: Ensure all code paths are tested -- **Test Failure Resolution**: Fix any failing tests before proceeding - -### Phase 6: Comprehensive Browser Testing with Content Verification (10-15 minutes) - -**🚨 CRITICAL: Don't just check HTTP status - verify actual page content renders correctly** - -#### Testing Process: - -**Step 1: Verify Server & Port** -- Get port from MCP: `mcp__wheels__wheels_server(action="status")` -- Construct base URL: `http://localhost:PORT` - -**Step 2: Test Homepage/Index** -```bash -# Check HTTP status -curl -s "http://localhost:PORT" -I # Should be 200 OK - -# Verify actual content appears (not just status code) -curl -s "http://localhost:PORT" | grep "Expected Title" -curl -s "http://localhost:PORT" | grep -c "article class" # Count records displayed - -# Verify frontend stack loaded -curl -s "http://localhost:PORT" | grep -E "(Tailwind|Alpine|HTMX)" -``` - -**Step 3: Test Individual Resource Pages** -```bash -# Test show page -curl -s "http://localhost:PORT/posts/2" -I # Should be 200 OK -curl -s "http://localhost:PORT/posts/2" | grep "Comments (" # Verify comments section - -# Test new page -curl -s "http://localhost:PORT/posts/new" -I # Should be 200 OK -curl -s "http://localhost:PORT/posts/new" | grep "Create" # Verify form title - -# Test edit page -curl -s "http://localhost:PORT/posts/2/edit" -I # Should be 200 OK -curl -s "http://localhost:PORT/posts/2/edit" | grep "Edit" # Verify form title -``` - -**Step 4: Verify Links Generate Correctly** -```bash -# Check what URLs linkTo generates -curl -s "http://localhost:PORT" | grep -o 'href="[^"]*posts[^"]*"' | head -5 - -# Verify links point to correct resources, not just /posts -``` - -**Step 5: Test Interactive Elements** -```bash -# Verify Alpine.js directives exist -curl -s "http://localhost:PORT/posts/2" | grep -E "x-data|@click" - -# Verify HTMX attributes if used -curl -s "http://localhost:PORT" | grep "hx-" -``` - -**Step 6: Verify Forms Have Required Elements** -```bash -# Check CSRF tokens present -curl -s "http://localhost:PORT/posts/new" | grep "csrf" - -# Check submit buttons exist -curl -s "http://localhost:PORT/posts/new" | grep "submit" - -# Verify validation error display areas exist -curl -s "http://localhost:PORT/posts/new" | grep "hasErrors" -``` - -**Step 7: Test Error Scenarios** -```bash -# Test 404 handling -curl -s "http://localhost:PORT/posts/99999" -I # Should redirect or show 404 - -# Test missing views don't cause 500 errors -curl -s "http://localhost:PORT/posts/1" -I # Should be 200, not 500 -``` - -**What Makes a Test PASS:** -- ✅ HTTP 200 status code -- ✅ Expected content appears in HTML (titles, records, forms) -- ✅ No error messages in page content -- ✅ Frontend libraries loaded (Tailwind, Alpine, HTMX) -- ✅ Links generate correct URLs with IDs -- ✅ Forms have all required elements (fields, labels, submit, CSRF) -- ✅ Interactive elements have proper attributes (x-data, @click, hx-) - -**What Makes a Test FAIL:** -- ❌ HTTP 500 Internal Server Error -- ❌ HTTP 302 redirect to unexpected location -- ❌ Page returns HTML but no actual content (empty lists) -- ❌ Error messages visible in page content -- ❌ Links missing or pointing to wrong URLs -- ❌ Forms missing fields or submit buttons -- ❌ Missing CSRF protection - -**Port Discovery Process:** -```javascript -// Step 1: Get server status via MCP (preferred) -mcp__wheels__wheels_server(action="status") // Returns port if running - -// Step 2: Fallback - Use CLI command -Bash("wheels server status") // Returns port and server status - -// Step 3: Fallback - Read server.json -Read("server.json") // Check "port" or "web.http.port" settings - -// Step 4: Default - Use 8080 if not specified -var port = discoveredPort || 8080; -var baseUrl = "http://localhost:" + port; -``` - -### Phase 7: Quality Assurance & Reporting (2-3 minutes) -- **⚠️ NOTE**: Anti-pattern detection should have already occurred during Phase 3 implementation -- **Final Anti-Pattern Scan**: One last check for any missed issues -- **Security Review**: Verify CSRF, authentication, input validation -- **Performance Analysis**: Check for N+1 queries, optimization opportunities -- **Documentation Compliance**: Validate against `.ai` documentation patterns -- **Test Coverage Report**: Generate detailed test coverage analysis -- **Comprehensive Reporting**: Generate detailed results with screenshots and test results - -### Phase 8: Rollback & Recovery (if needed) -**This phase only executes if previous phases encounter critical failures.** - -- **Test Failures**: Roll back code changes, analyze root cause, fix issues, re-run tests -- **Browser Test Failures**: Investigate root cause in screenshots, fix implementation, re-test -- **Migration Failures**: Run down migrations, fix schema issues, re-apply migrations -- **MCP Tool Failures**: Verify MCP server connection, restart if needed, document fallback to CLI -- **Complete Failure**: Document what worked and what didn't, propose alternative approach -- **Partial Success**: Document completed components, identify blocking issues, plan resolution - -**Rollback Strategy:** -```bash -# If MCP tools are available -mcp__wheels__wheels_migrate(action="down") # Rollback migrations -mcp__wheels__wheels_analyze(target="all") # Analyze current state - -# Document the issue -- What was attempted -- What failed and why -- What was successfully completed -- Recommended next steps -``` - -## View Generation Templates - -### Critical View Patterns - -All generated views MUST follow these patterns to avoid common errors: - -#### Index View Template (Resource List) -```cfm - - -#contentFor("title", "Resource List")# - -

Resources

- - -
- -
-

#linkTo(controller="resources", action="show", key=resources.id, text=resources.name)#

- - - -

#resourceAssoc.recordCount# associated items

-
-
-
- -

No resources found.

-
-
-``` - -#### Show View Template (Resource Detail) -```cfm - - - -#contentFor("title", "#resource.name# - Detail")# - -

#resource.name#

-

#resource.description#

- - -
- #linkTo(controller="resources", action="edit", key=resource.id, text="Edit")# - #linkTo(controller="resources", action="index", text="Back to List")# -
- - -

Associated Items (#associations.recordCount#)

- - -
#associations.name#
-
-
-
-``` - -#### New/Edit Form Template -```cfm - - -#contentFor("title", "Create Resource")# - -

Create Resource

- -#startFormTag(controller="resources", action="create", method="post")# - - -
- - #textField(objectName="resource", property="name", label=false)# - -

#resource.allErrors("name")[1]#

-
-
- - -
- - #textArea(objectName="resource", property="description", label=false)# - -

#resource.allErrors("description")[1]#

-
-
- - -
- -
- - -
- #submitTag(value="Create Resource")# - #linkTo(controller="resources", action="index", text="Cancel")# -
- -#endFormTag()# -
-``` - -#### Controller Template with Filters -```cfm -component extends="Controller" { - - function config() { - // Parameter verification - verifies(only="show,edit,update,delete", params="key", paramsTypes="integer"); - - // Filters - filters(through="findResource", only="show,edit,update,delete"); - } - - function index() { - // Load with associations to avoid N+1 queries - resources = model("Resource").findAll( - order="createdAt DESC", - include="association" - ); - } - - function show() { - // Resource loaded by filter - // Load associated records - associations = resource.association(order="createdAt ASC"); - } - - function new() { - resource = model("Resource").new(); - } - - function create() { - resource = model("Resource").new(params.resource); - - if (resource.save()) { - flashInsert(success="Resource created successfully!"); - redirectTo(action="show", key=resource.id); - } else { - flashInsert(error="Please correct the errors below."); - renderView(action="new"); - } - } - - function edit() { - // Resource loaded by filter - } - - function update() { - // Resource loaded by filter - if (resource.update(params.resource)) { - flashInsert(success="Resource updated successfully!"); - redirectTo(action="show", key=resource.id); - } else { - flashInsert(error="Please correct the errors below."); - renderView(action="edit"); - } - } - - function delete() { - // Resource loaded by filter - if (resource.delete()) { - flashInsert(success="Resource deleted successfully!"); - redirectTo(action="index"); - } else { - flashInsert(error="Unable to delete resource."); - redirectTo(action="show", key=resource.id); - } - } - - // Private filter - private function findResource() { - resource = model("Resource").findByKey(key=params.key); - - if (!isObject(resource)) { - flashInsert(error="Resource not found."); - redirectTo(action="index"); - } - } -} -``` - -### Migration Template (Database-Agnostic) -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - try { - // Use CFML date functions, not database-specific - var now = Now(); - var pastDate = DateAdd("d", -7, now); - - // Create table - t = createTable(name="resources", force=false); - t.string(columnNames="name", allowNull=false, limit=255); - t.text(columnNames="description", allowNull=true); - t.boolean(columnNames="active", default=false); - t.timestamps(); - t.create(); - - // Add indexes - addIndex(table="resources", columnNames="name"); - addIndex(table="resources", columnNames="active,createdAt"); - - // Seed data (if needed) using CFML date formatting - execute("INSERT INTO resources (name, description, active, createdAt, updatedAt) - VALUES ( - 'Sample Resource', - 'Description here', - 1, - TIMESTAMP '#DateFormat(now, "yyyy-mm-dd")# #TimeFormat(now, "HH:mm:ss")#', - TIMESTAMP '#DateFormat(now, "yyyy-mm-dd")# #TimeFormat(now, "HH:mm:ss")#' - )"); - - } catch (any e) { - local.exception = e; - } - - if (StructKeyExists(local, "exception")) { - transaction action="rollback"; - Throw(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any"); - } else { - transaction action="commit"; - } - } - } - - function down() { - dropTable("resources"); - } -} -``` - -## Anti-Pattern Prevention - -The workflow specifically prevents the two most common Wheels errors: - -### ❌ Mixed Argument Styles (PREVENTED) -```cfm -// BAD - will cause "Missing argument name" errors -hasMany("comments", dependent="delete"); -model("Post").findByKey(params.key, include="comments"); -``` - -### ✅ Consistent Argument Styles (ENFORCED) -```cfm -// GOOD - all named arguments -hasMany(name="comments", dependent="delete"); -model("Post").findByKey(key=params.key, include="comments"); - -// ALSO GOOD - all positional arguments -hasMany("comments"); -model("Post").findByKey(params.key); -``` - -### ❌ Query/Array Confusion (PREVENTED) -```cfm -// BAD - ArrayLen() on query objects - - -``` - -### ✅ Proper Query Handling (ENFORCED) -```cfm -// GOOD - use .recordCount for queries - - -``` - -## Success Criteria - -A feature is only considered complete when ALL of the following are true: -- [ ] ✅ **MCP tools were used exclusively (if `.mcp.json` exists)** -- [ ] ✅ **No CLI commands used when MCP available** -- [ ] ✅ **MCP server connection validated before starting** -- [ ] ✅ All relevant `.ai` documentation was consulted -- [ ] ✅ No anti-patterns detected in generated code -- [ ] ✅ **Comprehensive TestBox BDD test suite written and passing** -- [ ] ✅ **All model BDD specs pass (validations, associations, methods)** -- [ ] ✅ **All controller BDD specs pass (actions, filters, security)** -- [ ] ✅ **All integration BDD specs pass (user workflows, CRUD)** -- [ ] ✅ **Test coverage >= 90% for all components** -- [ ] ✅ All browser tests pass -- [ ] ✅ Every button, form, and link has been tested -- [ ] ✅ Responsive design works on mobile, tablet, desktop -- [ ] ✅ Security validations are in place -- [ ] ✅ Performance is acceptable -- [ ] ✅ Error scenarios are handled properly -- [ ] ✅ Screenshot evidence exists for all user flows -- [ ] ✅ Implementation follows Wheels conventions - -## Browser Testing Coverage - -The workflow automatically tests: - -### Navigation Testing -- Homepage load and layout -- All menu links and navigation paths -- Breadcrumb navigation -- Footer links and utility pages - -### CRUD Operations Testing -- Index pages (list views) -- Show pages (detail views) -- New/Create forms and submission -- Edit/Update forms and submission -- Delete actions and confirmations - -### Form Validation Testing -- Empty form submissions (should show errors) -- Partial form submissions -- Invalid data submissions -- Complete valid form submissions -- CSRF protection verification - -### Interactive Elements Testing -- JavaScript functionality -- Alpine.js components and interactions -- HTMX requests and responses -- Modal dialogs and dropdowns -- Dynamic content updates - -### Responsive Design Testing -- Mobile viewport (375x667) -- Tablet viewport (768x1024) -- Desktop viewport (1920x1080) -- Wide screen viewport (2560x1440) -- Mobile navigation (hamburger menus) - -### Error Scenario Testing -- 404 pages for nonexistent resources -- Authentication redirects -- Authorization failures -- Validation error displays -- Server error handling - -## Quality Gates - -### Automatic Rejection Criteria -Code will be automatically rejected if: -- Any mixed argument styles are detected -- Any `ArrayLen()` calls on model associations exist -- **Any TestBox BDD spec fails** -- **Test coverage is below 90%** -- **Missing BDD specs for any component** -- Any browser test fails -- Any security check fails -- Any anti-pattern is detected -- Routes don't follow RESTful conventions - -### Performance Requirements -- Pages should load without obvious delays -- Forms should submit without timeout errors -- No N+1 query patterns detected (check for missing `include` in model calls) -- Database queries should use indexes where appropriate -- Responsive design should not cause layout shifts - -### Security Requirements -- CSRF protection must be enabled -- All forms must include CSRF tokens -- Authentication filters must be present -- Input validation must be implemented -- SQL injection prevention must be verified - -## TestBox BDD Testing Requirements - -### Mandatory BDD Test Structure - -Every component MUST have comprehensive TestBox BDD specs using the following structure: - -#### Model Specs (`/tests/specs/models/`) -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - // Setup database and test environment - application.testbox = new testbox.system.TestBox(); - } - - function afterAll() { - // Cleanup test data - } - - function run() { - describe("Post Model", function() { - - beforeEach(function() { - variables.post = model("Post").new(); - }); - - afterEach(function() { - if (isObject(variables.post) && variables.post.isPersisted()) { - variables.post.delete(); - } - }); - - describe("Validations", function() { - it("should require title", function() { - variables.post.title = ""; - expect(variables.post.valid()).toBeFalse(); - expect(variables.post.allErrors()).toHaveKey("title"); - }); - - it("should require content", function() { - variables.post.content = ""; - expect(variables.post.valid()).toBeFalse(); - expect(variables.post.allErrors()).toHaveKey("content"); - }); - - it("should require unique slug", function() { - var existingPost = model("Post").create({ - title: "Test Post", - content: "Test content", - slug: "test-slug", - published: false - }); - - variables.post.slug = "test-slug"; - expect(variables.post.valid()).toBeFalse(); - expect(variables.post.allErrors()).toHaveKey("slug"); - - existingPost.delete(); - }); - }); - - describe("Associations", function() { - it("should have many comments", function() { - expect(variables.post.comments()).toBeQuery(); - }); - - it("should delete associated comments", function() { - var savedPost = model("Post").create({ - title: "Test Post", - content: "Test content", - published: false - }); - - var comment = model("Comment").create({ - content: "Test comment", - authorName: "Test Author", - authorEmail: "test@example.com", - postId: savedPost.id - }); - - expect(savedPost.comments().recordCount).toBe(1); - savedPost.delete(); - expect(model("Comment").findByKey(comment.id)).toBeFalse(); - }); - }); - - describe("Methods", function() { - it("should generate excerpt", function() { - variables.post.content = "

This is a long content that should be truncated at some point for the excerpt.

"; - expect(len(variables.post.excerpt(20))).toBeLTE(23); // 20 + "..." - }); - - it("should auto-generate slug from title", function() { - variables.post.title = "This is a Test Title!"; - variables.post.setSlugAndPublishDate(); - expect(variables.post.slug).toBe("this-is-a-test-title"); - }); - }); - }); - } -} -``` - -#### Controller Specs (`/tests/specs/controllers/`) -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - application.testbox = new testbox.system.TestBox(); - } - - function run() { - describe("Posts Controller", function() { - - beforeEach(function() { - // Setup test data - variables.testPost = model("Post").create({ - title: "Test Post", - content: "Test content for controller testing", - published: true, - publishedAt: now() - }); - }); - - afterEach(function() { - if (isObject(variables.testPost)) { - variables.testPost.delete(); - } - }); - - describe("index action", function() { - it("should load published posts", function() { - var controller = controller("Posts"); - controller.index(); - - expect(controller.posts).toBeQuery(); - expect(controller.posts.recordCount).toBeGTE(1); - }); - - it("should order posts by publishedAt DESC", function() { - var newerPost = model("Post").create({ - title: "Newer Post", - content: "Newer content", - published: true, - publishedAt: dateAdd("h", 1, now()) - }); - - var controller = controller("Posts"); - controller.index(); - - expect(controller.posts.title[1]).toBe("Newer Post"); - newerPost.delete(); - }); - }); - - describe("show action", function() { - it("should load post and comments", function() { - var controller = controller("Posts"); - controller.params.key = variables.testPost.id; - controller.show(); - - expect(controller.post.id).toBe(variables.testPost.id); - expect(controller.comments).toBeQuery(); - }); - }); - - describe("create action", function() { - it("should create valid post", function() { - var controller = controller("Posts"); - controller.params.post = { - title: "New Test Post", - content: "New test content", - published: true - }; - - var initialCount = model("Post").count(); - controller.create(); - - expect(model("Post").count()).toBe(initialCount + 1); - - // Cleanup - var newPost = model("Post").findOne(where="title = 'New Test Post'"); - if (isObject(newPost)) { - newPost.delete(); - } - }); - - it("should handle validation errors", function() { - var controller = controller("Posts"); - controller.params.post = { - title: "", // Invalid - empty title - content: "Test content" - }; - - controller.create(); - expect(controller.post.hasErrors()).toBeTrue(); - }); - }); - }); - } -} -``` - -#### Integration Specs (`/tests/specs/integration/`) -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Blog Workflow Integration", function() { - - beforeEach(function() { - // Setup clean test environment - }); - - afterEach(function() { - // Cleanup test data - }); - - describe("Complete post lifecycle", function() { - it("should create, publish, and delete post", function() { - // Create post - var post = model("Post").create({ - title: "Integration Test Post", - content: "Integration test content", - published: false - }); - - expect(post.isNew()).toBeFalse(); - expect(post.published).toBeFalse(); - - // Publish post - post.update({published: true, publishedAt: now()}); - expect(post.published).toBeTrue(); - - // Add comment - var comment = model("Comment").create({ - content: "Integration test comment", - authorName: "Test Author", - authorEmail: "test@example.com", - postId: post.id - }); - - expect(post.comments().recordCount).toBe(1); - - // Delete post (should cascade delete comments) - post.delete(); - expect(model("Comment").findByKey(comment.id)).toBeFalse(); - }); - }); - - describe("Form validation workflow", function() { - it("should prevent invalid post creation", function() { - var post = model("Post").new({ - title: "", // Invalid - content: "x" // Too short - }); - - expect(post.save()).toBeFalse(); - expect(post.allErrors()).toHaveKey("title"); - expect(post.allErrors()).toHaveKey("content"); - }); - }); - }); - } -} -``` - -### Test Execution Requirements - -#### Mandatory Test Commands -All tests MUST be executed and pass before completion: - -**If MCP tools available (preferred):** -```javascript -// Run all model specs -mcp__wheels__wheels_test(type="models", reporter="json") - -// Run all controller specs -mcp__wheels__wheels_test(type="controllers", reporter="json") - -// Run all integration specs -mcp__wheels__wheels_test(type="integration", reporter="json") - -// Run complete test suite -mcp__wheels__wheels_test(type="all", reporter="json") -``` - -**If MCP tools NOT available (fallback only):** -```bash -# Run all model specs -wheels test model --reporter=json - -# Run all controller specs -wheels test controller --reporter=json - -# Run all integration specs -wheels test integration --reporter=json - -# Run complete test suite -wheels test all --reporter=json -``` - -#### Test Coverage Requirements -- **Models**: 100% coverage of all public methods, validations, and associations -- **Controllers**: 100% coverage of all actions and filters -- **Integration**: 90% coverage of complete user workflows -- **Overall**: Minimum 90% total coverage across all components - -#### Test Data Management -- Use TestBox's `beforeEach()` and `afterEach()` for test isolation -- Create test fixtures for complex scenarios -- Always clean up test data to prevent test pollution -- Use database transactions for faster test execution - -## Error Recovery System - -When errors occur during any phase: - -1. **Identify Error Type**: Syntax, logic, pattern, or security error -2. **Load Recovery Documentation**: Load relevant `.ai` documentation for the error -3. **Apply Documented Solution**: Use established patterns from documentation -4. **Retry Operation**: Attempt the operation with corrected approach -5. **Log Pattern**: Document the error pattern for future prevention - -### Common Recovery Flows - -#### Mixed Argument Error Recovery -``` -Error: "Missing argument name" detected -→ Load: .ai/wheels/troubleshooting/common-errors.md -→ Fix: Convert to consistent argument style -→ Retry: Code generation with corrected pattern -→ Validate: Syntax check passes -``` - -#### Query/Array Confusion Recovery -``` -Error: ArrayLen() on query object detected -→ Load: .ai/wheels/models/data-handling.md -→ Fix: Use .recordCount and proper loop syntax -→ Retry: View generation with correct patterns -→ Validate: Browser test confirms functionality -``` - -#### MCP Connection Failure Recovery -``` -Error: MCP server not responding -→ Check: Server running via mcp__wheels__wheels_server(action="status") -→ Fix: Restart server or verify .mcp.json configuration -→ Validate: Test MCP connection before retrying -→ Fallback: Document why MCP unavailable, use CLI tools as last resort -``` - -#### TestBox BDD Test Failure Recovery -``` -Error: BDD specs failing or missing -→ Load: .ai/wheels/testing/ documentation -→ Fix: Write comprehensive BDD specs for all components -→ Retry: Run complete test suite -→ Validate: All tests pass with 90%+ coverage -``` - -#### Test Coverage Insufficient Recovery -``` -Error: Test coverage below 90% -→ Analyze: Identify untested code paths -→ Fix: Add BDD specs for missing scenarios -→ Retry: Run test suite with coverage analysis -→ Validate: Coverage meets minimum requirements -``` - -## Implementation Strategy - -### Documentation Loading Strategy -1. **Universal Critical Documentation** (always loaded first): - - `.ai/wheels/troubleshooting/common-errors.md` - - `.ai/wheels/patterns/validation-templates.md` - - `.ai/wheels/workflows/pre-implementation.md` - -2. **Component-Specific Documentation** (loaded based on task analysis): - - Models: `.ai/wheels/models/architecture.md`, `associations.md`, `validations.md` - - Controllers: `.ai/wheels/controllers/architecture.md`, `rendering.md`, `filters.md` - - Views: `.ai/wheels/views/data-handling.md`, `architecture.md`, `forms.md` - - Migrations: `.ai/wheels/database/migrations/creating-migrations.md` - -3. **Feature-Specific Documentation** (loaded as needed): - - Authentication: `.ai/wheels/models/user-authentication.md` - - Security: `.ai/wheels/security/csrf-protection.md` - - Forms: `.ai/wheels/views/helpers/forms.md` - -### Task Type Detection -The workflow analyzes the task description for: -- **Model Indicators**: "model", "User", "Post", "association", "validation" -- **Controller Indicators**: "controller", "action", "CRUD", "API", "filter" -- **View Indicators**: "view", "template", "form", "layout", "responsive" -- **Feature Indicators**: "auth", "admin", "search", "email", "upload" - -### Browser Testing Strategy -Based on application type detected: -- **Blog Applications**: Post CRUD, commenting, navigation -- **E-commerce Applications**: Product catalog, shopping cart, checkout -- **Admin Applications**: User management, authentication, dashboards -- **API Applications**: Endpoint testing, JSON responses, authentication - -## Comparison Benefits vs MCP Tool - -### Advantages of Slash Command Approach -- **Flexibility**: Claude Code can adapt the workflow dynamically -- **Error Handling**: Better error recovery and human-readable feedback -- **Documentation Integration**: Direct access to `.ai` folder without MCP resource limitations -- **Comprehensive Testing**: TestBox BDD specs + Browser testing + Integration testing -- **Test Coverage**: Mandatory 90%+ coverage with detailed analysis -- **Quality Assurance**: No feature complete without passing test suite -- **Reporting**: Rich, detailed reporting with screenshots, test results, and coverage analysis -- **Learning**: Users see the complete process and can learn from it - -### Testing Strategy -Run both approaches on the same task: -``` -/wheels_execute create a blog with posts and comments -vs -mcp__wheels__develop(task="create a blog with posts and comments") -``` - -Compare results on: -- Code quality and adherence to patterns -- Test coverage and browser testing thoroughness -- Error prevention and pattern consistency -- Implementation time and reliability -- User experience and learning value - -This slash command provides a systematic, comprehensive approach to Wheels development that ensures professional quality results with complete testing coverage. - -## 📁 Spec Versioning System - -### How Specs Are Stored and Used - -**Directory Structure:** -``` -.specs/ -├── 20250930-163000-blog-posts-comments.md (Initial blog implementation) -├── 20250930-170000-add-user-authentication.md (Added authentication) -├── 20250930-173000-add-tags-to-posts.md (Added tagging feature) -├── 20250930-180000-add-search-functionality.md (Added search) -└── current.md → 20250930-180000-add-search-functionality.md (Symlink to latest) -``` - -**Each Spec File Contains:** -```markdown -# Feature Specification: [Feature Name] - -**Created:** 2025-09-30 16:30:00 -**Status:** completed -**Estimated Time:** 20-30 minutes -**Actual Time:** 22 minutes -**Started:** 2025-09-30 16:31:00 -**Completed:** 2025-09-30 16:53:00 - -## User Request -"[original user request verbatim]" - -## Previous Specs -- [20250930-163000-blog-posts-comments.md](.specs/20250930-163000-blog-posts-comments.md) - -## This Spec Builds On -- Post and Comment models (from 20250930-163000) -- Tailwind CSS layout (from 20250930-163000) - -## Components to Add -- [List of NEW components] - -## Components to Modify -- [List of MODIFIED components] - -## Database Schema -[Complete schema for new/modified tables] - -## Models -[Model specifications] - -## Controllers -[Controller specifications] - -## Views -[View specifications] - -## Routes -[Route changes] - -## Implementation Progress -**Models:** ✅ Complete -**Controllers:** ✅ Complete -**Views:** ✅ Complete -**Tests:** ✅ Complete - -## Files Created -[Clickable links to all created files] - -## Test Results -**All Tests Passed:** ✅ -**Browser Tests:** ✅ All pages verified -**Test Coverage:** 92% -``` - -### Incremental Development Example - -**First Feature:** -```bash -/wheels_execute create a blog with posts and comments -``` -Creates: `.specs/20250930-163000-blog-posts-comments.md` - -**Second Feature:** -```bash -/wheels_execute add user authentication with login/logout -``` -- Reads `.specs/current.md` to understand existing structure -- Sees: Post model, Comment model, Posts controller, Comments controller already exist -- Creates: `.specs/20250930-170000-add-user-authentication.md` -- References previous spec in "Previous Specs" section -- Lists what it builds on in "This Spec Builds On" section -- Only creates NEW components (User model, Sessions controller, auth views) -- Only MODIFIES what needs changing (Posts controller to add auth filter) - -**Third Feature:** -```bash -/wheels_execute add tags to posts with many-to-many relationship -``` -- Reads `.specs/current.md` (now pointing to authentication spec) -- Reads previous spec to see Post model structure -- Creates: `.specs/20250930-173000-add-tags-to-posts.md` -- References: 20250930-163000 (original blog) and 20250930-170000 (authentication) -- Builds on: Post model (adds hasManyThrough), existing Tailwind layout -- Creates: Tag model, PostTag join model, Tags controller, tag views -- Modifies: Post model (add association), posts/new.cfm and posts/edit.cfm (add tag picker) - -### Benefits of This Approach - -1. **Complete Audit Trail**: See exactly what was built, when, and why -2. **Context Awareness**: Each new feature understands what already exists -3. **Avoid Duplication**: Won't recreate models/controllers that already exist -4. **Smart Modifications**: Knows to modify existing files rather than create new ones -5. **Rollback Capability**: Can see state at any point in project history -6. **Documentation**: Automatic project documentation showing evolution -7. **Team Collaboration**: Share specs to show what was implemented -8. **Time Tracking**: Accurate implementation time for future estimation - -### Symlink Management - -**Creating the Symlink:** -```bash -# After spec is approved and implementation starts -ln -sf 20250930-163000-blog-posts-comments.md .specs/current.md -``` - -**Updating the Symlink:** -```bash -# When new feature is started, symlink points to new spec -ln -sf 20250930-170000-add-user-authentication.md .specs/current.md -``` - -**Why current.md is Important:** -- Always points to the latest approved/in-progress spec -- Phase 1 loads this to understand current project state -- Makes it easy to know "what's the current state without sorting by timestamp" - -### Handling Spec Modifications - -**If User Requests Changes During Approval (Phase 3):** -```markdown -User: "change: use Bootstrap instead of Tailwind" - -Claude Code: -1. Update the spec in-memory -2. Re-present updated spec for approval -3. Do NOT save to .specs/ until approved -4. Once approved, save with original timestamp -``` - -**If Implementation Encounters Issues:** -```markdown -During Phase 5: Discover that Comment model needs additional field - -Claude Code: -1. Update .specs/current.md with modification: - ``` - ## Modifications During Implementation - - **Comment model**: Added `approved` boolean field for moderation - ``` -2. Continue implementation -3. Final spec shows both original plan and modifications made -``` - -**Spec Status Lifecycle:** -``` -draft → approved → in-progress → completed - ↘ modified (if changes occur during implementation) -``` - -### Version Control Integration - -**Recommended .gitignore:** -```gitignore -# Don't ignore specs - they're valuable documentation -# .specs/ - -# But do ignore symlinks if they cause issues -.specs/current.md -``` - -**Committing Specs:** -```bash -# After each feature completion -git add .specs/20250930-163000-blog-posts-comments.md -git commit -m "Add spec for blog posts and comments feature" - -# Specs become part of project documentation -# Can be referenced in commit messages, pull requests, etc. -``` - ---- - -## 📖 Complete Usage Example - -### User Request: -``` -/wheels_execute create a blog with posts and comments, use Tailwind CSS and Alpine.js -``` - -### Claude Code Response (Phase 2 - Specification): - -```markdown -## 📋 Implementation Specification - -I've analyzed your request and created the following specification: - -### Database Schema - -**Posts Table:** -- id (primary key) -- title (string, required, 3-200 chars) -- slug (string, unique, auto-generated) -- content (text, required, min 10 chars) -- published (boolean, default false) -- publishedAt (datetime, nullable) -- createdAt, updatedAt (timestamps) -- Indexes: slug (unique), published+publishedAt - -**Comments Table:** -- id (primary key) -- content (text, required, 3-1000 chars) -- authorName (string, required, 2-100 chars) -- authorEmail (string, required, email format) -- postId (foreign key → posts) -- createdAt, updatedAt (timestamps) -- Indexes: postId, createdAt - -### Models - -**Post Model:** -- hasMany(name="comments", dependent="delete") -- Validations: presence, uniqueness, length -- Methods: generateSlug(), excerpt(), setSlugAndPublishDate() -- Callbacks: beforeValidationOnCreate - -**Comment Model:** -- belongsTo(name="post") -- Validations: presence, email format, length -- Methods: getGravatarUrl() - -### Controllers - -**Posts:** index, show, new, create, edit, update, delete (with findPost filter) -**Comments:** create, delete (redirects to post show) - -### Views - -**Layout:** Tailwind CSS, Alpine.js, responsive navigation -**Posts:** index (grid), show (with comments), new (form), edit (form) -All forms include validation error displays, CSRF tokens, submit/cancel buttons - -### Routes - -- Root: / → posts#index -- Resources: posts, comments -- Wildcard enabled - -### Frontend Stack - -- Tailwind CSS (utility styling) -- Alpine.js (comment form toggle, mobile menu) -- HTMX (available for enhancements) - -### Sample Data - -10 tech blog posts (HTMX, Tailwind, Security, Testing, DevOps, etc.) - ---- - -## 📝 Implementation Tasks (15 tasks) - -If approved, I will complete these tasks with incremental testing: - -1. ✅ Generate Post model with validations and associations -2. ✅ Generate Comment model with validations and associations -3. ✅ Create migrations (posts, comments, seed 10 posts) -4. ✅ Run migrations and verify tables created -5. ✅ Generate Posts controller with CRUD actions -6. ✅ Generate Comments controller (create, delete) -7. ✅ Create layout with Tailwind + Alpine.js -8. ✅ Create posts/index.cfm and test (200 OK, 10 posts) -9. ✅ Create posts/show.cfm and test (200 OK, comments section) -10. ✅ Create posts/new.cfm and test (200 OK, form renders) -11. ✅ Create posts/edit.cfm and test (200 OK, pre-populated) -12. ✅ Configure routes (root, resources) -13. ✅ Test complete CRUD workflow (create, edit, delete) -14. ✅ Test comment creation and deletion -15. ✅ Final browser testing (all URLs, content, frontend stack) - -**Estimated Time:** 20-30 minutes - ---- - -**Please review and respond:** -- Type **"approve"** to begin implementation -- Type **"change: [description]"** to request modifications -- Ask questions about any part of the spec -``` - -### User Response: -``` -approve -``` - -### Claude Code Implementation (Phase 4-6): - -```markdown -## 🔄 Implementation Started - -Creating task list... - -**Tasks:** -✅ Generate Post model with validations -✅ Generate Comment model with validations -✅ Create database migrations -✅ Run migrations and verify tables -✅ Generate Posts controller with all CRUD actions -✅ Generate Comments controller (create, delete) -✅ Create layout with Tailwind CSS, Alpine.js -🔄 Create posts/index.cfm view and test (In Progress) -⏳ Create posts/show.cfm view with comments and test -⏳ Create posts/new.cfm form and test -⏳ Create posts/edit.cfm form and test -⏳ Configure routes -⏳ Test complete CRUD workflow -⏳ Test comment functionality -⏳ Final browser testing - ---- - -Testing posts/index.cfm... -```bash -curl -s http://localhost:58635 -I -# HTTP/1.1 200 OK ✅ - -curl -s http://localhost:58635 | grep "Latest Tech Posts" -# Found: "Latest Tech Posts" ✅ - -curl -s http://localhost:58635 | grep -c "article class" -# Count: 10 ✅ -``` - -✅ posts/index.cfm verified! - -Moving to next task... - -[Continues with each task...] -``` - -### Final Result (Phase 8): - -```markdown -## ✅ Blog Implementation Complete! - -### What Was Built: - -**Database:** -- ✅ Posts table with slug (unique) and published+publishedAt indexes -- ✅ Comments table with postId and createdAt indexes -- ✅ 10 tech blog posts seeded (HTMX, Tailwind, Security, etc.) - -**Models:** -- ✅ Post.cfc (validations, associations, slug generation, excerpt method) -- ✅ Comment.cfc (validations, belongsTo, Gravatar support) - -**Controllers:** -- ✅ Posts.cfc (full CRUD with findPost filter, flash messages) -- ✅ Comments.cfc (create, delete with redirects) - -**Views:** -- ✅ layout.cfm (Tailwind CSS, Alpine.js, responsive nav) -- ✅ posts/index.cfm (grid layout, post cards) -- ✅ posts/show.cfm (full post, comments section, Alpine.js toggle) -- ✅ posts/new.cfm (form with validation errors) -- ✅ posts/edit.cfm (form pre-populated) - -**Routes:** -- ✅ / → posts#index -- ✅ RESTful resources (posts, comments) - -### Test Results: - -**All pages working:** -- ✅ http://localhost:58635 (200 OK, 10 posts displayed) -- ✅ http://localhost:58635/posts/2 (200 OK, comments section) -- ✅ http://localhost:58635/posts/new (200 OK, form rendered) -- ✅ http://localhost:58635/posts/2/edit (200 OK, pre-populated) - -**Frontend verified:** -- ✅ Tailwind CSS styling applied -- ✅ Alpine.js interactive elements working -- ✅ Mobile responsive - -### What You Can Do Now: - -1. Visit **http://localhost:58635** to see your blog -2. Browse 10 tech articles -3. Click "Write Post" to create new posts -4. Edit or delete posts -5. Add comments with Gravatar avatars -6. Test on mobile (hamburger menu works) - -### Files Created: - -- Models: [Post.cfc](/Users/peter/projects/ws/blog/app/models/Post.cfc), [Comment.cfc](/Users/peter/projects/ws/blog/app/models/Comment.cfc) -- Controllers: [Posts.cfc](/Users/peter/projects/ws/blog/app/controllers/Posts.cfc), [Comments.cfc](/Users/peter/projects/ws/blog/app/controllers/Comments.cfc) -- Views: [layout.cfm](/Users/peter/projects/ws/blog/app/views/layout.cfm), posts/*.cfm -- Migrations: 3 migration files -- Routes: [routes.cfm](/Users/peter/projects/ws/blog/config/routes.cfm) updated - -**Implementation Time:** 22 minutes -**All 15 tasks completed successfully!** ✅ -``` - ---- - -## 🎯 Key Advantages of Spec-Driven Approach - -1. **Transparency:** User sees exactly what will be built before any code is written -2. **Control:** User can request changes to the spec before implementation -3. **Progress Visibility:** Real-time task tracking shows what's done and what's next -4. **Quality:** Each task is tested before moving forward -5. **Documentation:** Complete spec and results report for future reference -6. **Confidence:** User knows the feature is thoroughly tested and working - -This approach transforms `/wheels_execute` from a "black box" into a **collaborative, transparent development process**. \ No newline at end of file diff --git a/.claude/commands/wheels_spec.md b/.claude/commands/wheels_spec.md new file mode 100644 index 0000000000..79a7975873 --- /dev/null +++ b/.claude/commands/wheels_spec.md @@ -0,0 +1,226 @@ +# /wheels_spec - Analyze Requirements and Generate Specification + +## Description +Parse a natural language request into a detailed implementation specification for a Wheels application feature. Present the spec for user approval before any code is written. + +## Usage +``` +/wheels_spec [description of what to build] +``` + +## Examples +``` +/wheels_spec create a blog with posts and comments +/wheels_spec add user authentication with login/logout +/wheels_spec build a product catalog with categories and search +``` + +## Workflow + +### Step 1: Understand Existing State + +Before planning anything, understand what already exists. + +**Check for previous specs:** +``` +Glob(pattern=".specs/*.md") +``` +If `.specs/` exists and has files, read `current.md` to understand what models, controllers, views, and routes already exist. This prevents recreating things that are already built. + +**Scan existing codebase:** +``` +Glob(pattern="app/models/*.cfc") +Glob(pattern="app/controllers/*.cfc") +Glob(pattern="app/views/**/*.cfm") +Read("config/routes.cfm") +``` + +Build a mental model of what exists: +- Which models are defined and what associations do they have? +- Which controllers exist and what actions do they handle? +- Which views are in place? +- What routes are configured? + +### Step 2: Parse the User Request + +Extract from the natural language description: +- **Entities**: What data models are needed (User, Post, Comment, Product, etc.) +- **Relationships**: How entities relate (Post hasMany Comments, User belongsTo Role) +- **CRUD scope**: Which resources need full CRUD vs partial (Comments might only need create/delete) +- **Special features**: Authentication, file upload, email, API endpoints, search +- **Frontend preferences**: Tailwind, Bootstrap, Alpine.js, HTMX, or plain HTML + +Categorize each component as: +- **NEW**: Does not exist yet, must be created +- **MODIFY**: Exists but needs changes (e.g., add an association to an existing model) +- **EXISTS**: Already in place, no changes needed + +### Step 3: Generate the Specification + +Create a structured specification covering all components. The spec must be concrete enough that `/wheels_build` can implement it without ambiguity. + +#### 3a: Database Schema + +For each NEW or MODIFIED table, specify: +- Table name +- Column names, types, constraints (allowNull, default, limit) +- Indexes (which columns, unique or not) +- Foreign keys (references, on delete behavior) +- Whether timestamps() are needed + +Column type reference: +- `string` - varchar, use `limit` for max length +- `text` - long text content +- `integer` - whole numbers, use for foreign keys +- `decimal` - use `precision` and `scale` for money +- `boolean` - true/false with default +- `date` / `datetime` - date values +- `timestamps()` - creates createdAt and updatedAt + +#### 3b: Models + +For each model, specify: +- **Associations**: Full function signatures with all named parameters + ``` + hasMany(name="comments", dependent="delete") + belongsTo(name="post") + ``` +- **Validations**: Which properties, what rules + ``` + validatesPresenceOf("title,content") + validatesUniquenessOf(property="slug") + validatesLengthOf(property="title", minimum=3, maximum=200) + validatesFormatOf(property="email", regEx="^[\w\.-]+@[\w\.-]+\.\w+$") + ``` +- **Callbacks**: When they fire and what they do + ``` + beforeValidationOnCreate("generateSlug") + ``` +- **Custom methods**: Name, purpose, return value + +#### 3c: Controllers + +For each controller, specify: +- **Actions**: Which actions (index, show, new, create, edit, update, delete, custom) +- **Filters**: Which filter functions, which actions they apply to + ``` + filters(through="findPost", only="show,edit,update,delete") + ``` +- **Parameter verification**: + ``` + verifies(only="show,edit,update,delete", params="key", paramsTypes="integer") + ``` +- **Flash messages**: Success/error messages for each action +- **Redirects**: Where each action redirects on success/failure + +#### 3d: Views + +For each view file, specify: +- **Filename**: e.g., `app/views/posts/index.cfm` +- **Purpose**: What the view displays +- **Data dependencies**: What variables it expects (set via `cfparam`) +- **Key elements**: Forms, lists, links, interactive components +- **Association access pattern**: How to access related data in loops + +View checklist for forms: +- Field labels +- Validation error display for each field +- CSRF token (via `authenticityToken()` or form helpers) +- Submit and Cancel buttons + +#### 3e: Routes + +Specify route changes needed in `config/routes.cfm`: +- Resource routes: `.resources("posts")` +- Root route: `.root(to="posts##index", method="get")` +- Custom named routes if any +- Route ordering (resources before wildcard) + +#### 3f: Sample Data (if applicable) + +If the feature benefits from seed data: +- How many records +- What fields populated +- Relationships between seed records + +#### 3g: Test Plan + +List what needs testing: +- Model validations (presence, uniqueness, format, length) +- Model associations (create, access, dependent delete) +- Controller actions (each returns expected status) +- View rendering (pages load without errors) +- Route mapping (URLs resolve correctly) + +### Step 4: Present for Approval + +Format the spec as readable markdown and present it to the user. Include: + +1. Summary of what will be built (NEW components) and what will be modified (MODIFY components) +2. The full specification from Step 3 +3. An ordered task list showing the implementation sequence +4. Estimated scope (number of files to create/modify) + +End with: +``` +Please review this specification: +- Type "approve" to proceed with implementation (use /wheels_build) +- Describe any changes you want and I will revise the spec +- Ask questions about any part +``` + +### Step 5: Save the Spec + +Once the user approves: + +1. Create `.specs/` directory if it doesn't exist +2. Generate filename: `YYYYMMDD-HHMMSS-feature-description.md` (use kebab-case for feature description) +3. Write the spec file with this header: +```markdown +# Feature Specification: [Feature Name] + +**Created:** [timestamp] +**Status:** approved + +## User Request +"[original request verbatim]" + +## Previous Specs +- [list any previous specs referenced, or "None"] + +## Builds On +- [list existing components this feature depends on] + +[Full specification content from Step 3] +``` +4. Create/update `.specs/current.md` as a copy (not symlink) pointing to this content + +After saving, tell the user: +``` +Spec saved to .specs/[filename].md +Run /wheels_build to implement this specification. +``` + +## What This Command Does NOT Do + +- Does not generate any code +- Does not run migrations +- Does not create models, controllers, or views +- Does not modify routes +- Does not run tests + +This command only analyzes, plans, and documents. Use `/wheels_build` to implement and `/wheels_validate` to verify. + +## Handling Revisions + +If the user requests changes after seeing the spec: +1. Update the relevant sections +2. Re-present the full updated spec +3. Wait for approval again +4. Only save to `.specs/` after final approval + +## Integration with Other Commands + +- **Output**: A saved `.specs/*.md` file that `/wheels_build` reads as its input +- **Input to /wheels_build**: The spec provides the exact blueprint for implementation +- **Input to /wheels_validate**: The spec provides the checklist for what to verify diff --git a/.claude/commands/wheels_validate.md b/.claude/commands/wheels_validate.md new file mode 100644 index 0000000000..a80322b18f --- /dev/null +++ b/.claude/commands/wheels_validate.md @@ -0,0 +1,211 @@ +# /wheels_validate - Validate Implementation + +## Description +Verify that a Wheels application feature is correctly implemented. Runs tests, scans for anti-patterns, checks routes, verifies associations, and reports findings with file:line references. + +## Usage +``` +/wheels_validate # Validate entire app +/wheels_validate models # Validate only models +/wheels_validate controllers # Validate only controllers +/wheels_validate views # Validate only views +/wheels_validate migrations # Validate only migrations +``` + +## Workflow + +### Step 1: Run the Test Suite + +Detect available tools (check for `.mcp.json`). + +**Run tests:** +``` +mcp__wheels__wheels_test() # MCP +wheels test run # CLI +``` + +Report: number of tests run, passed, failed, errored. For any failures, include the test name and error message. + +### Step 2: Scan for Anti-Patterns + +Scan the codebase for the top 10 Wheels anti-patterns. Use Grep to search across files and report every match with the file path and line number. + +#### Anti-Pattern 1: Mixed Argument Styles +Function calls that mix positional and named parameters. + +**Search models for:** +``` +Grep: hasMany\("[^"]+",\s*\w+= in app/models/*.cfc +Grep: belongsTo\("[^"]+",\s*\w+= in app/models/*.cfc +Grep: hasOne\("[^"]+",\s*\w+= in app/models/*.cfc +Grep: validatesPresenceOf\("[^"]+",\s*\w+= in app/models/*.cfc +``` + +**Fix:** Use all-named parameters when any named parameter is needed. + +#### Anti-Pattern 2: Query/Array Confusion in Views +Using array functions on query objects or array loop syntax on queries. + +**Search views for:** +``` +Grep: ArrayLen\( in app/views/**/*.cfm +Grep: cfloop\s+array= in app/views/**/*.cfm +``` + +**Fix:** Use `.recordCount` for queries and `` syntax. + +#### Anti-Pattern 3: Missing cfparam Declarations +Views that use variables without declaring them with cfparam. + +**Check each view file:** +Read the file, identify which variables are used in the template, verify each has a corresponding `` at the top. + +#### Anti-Pattern 4: Database-Specific SQL in Migrations +MySQL/PostgreSQL/MSSQL-specific functions in migration files. + +**Search migrations for:** +``` +Grep: DATE_SUB|DATE_ADD|NOW\(\)|CURDATE|GETDATE|DATEADD in app/migrator/migrations/*.cfc +Grep: AUTO_INCREMENT|SERIAL|IDENTITY in app/migrator/migrations/*.cfc +``` + +**Fix:** Use CFML date functions (`DateAdd()`, `Now()`, `DateFormat()`) with `TIMESTAMP` formatting. + +#### Anti-Pattern 5: Missing CSRF Protection +Controllers that handle form submissions without CSRF protection. + +**Check:** For each controller that has a `create`, `update`, or `delete` action, verify the `config()` function includes `protectsFromForgery()`. + +``` +Grep: protectsFromForgery in app/controllers/*.cfc +``` + +Controllers with form-handling actions but no `protectsFromForgery()` are flagged. + +#### Anti-Pattern 6: Non-Private Filter Functions +Controller filter functions that are not marked as private. + +**Search controllers for:** +Read each controller. Find functions referenced in `filters(through="functionName")`. Verify each referenced function has the `private` access modifier. + +#### Anti-Pattern 7: String Boolean Values in Migrations +CLI-generated migrations often have boolean values as strings. + +**Search migrations for:** +``` +Grep: force='false'|force="false"|id='true'|id="true" in app/migrator/migrations/*.cfc +``` + +**Fix:** Remove these parameters entirely (defaults are correct) or use actual boolean values. + +#### Anti-Pattern 8: Missing Error Handling in Controllers +Create/update actions that don't handle validation failures. + +**Check:** For each `create()` and `update()` action, verify there's an `else` branch that calls `renderView()` to re-display the form with errors. + +#### Anti-Pattern 9: N+1 Query Patterns +Loading associations inside loops instead of using `include` in the initial query. + +**Search views for:** +``` +Grep: model\("[^"]+"\)\.find in app/views/**/*.cfm +``` + +If model queries appear inside `cfloop` blocks, flag as potential N+1. + +**Fix:** Use `include="association"` in the controller's `findAll()` call. + +#### Anti-Pattern 10: Hardcoded URLs Instead of Route Helpers +Using hardcoded href paths instead of `linkTo()` or `urlFor()`. + +**Search views for:** +``` +Grep: href="/[a-z] in app/views/**/*.cfm +``` + +Exclude external URLs (https://) and anchor links (#). Flag internal hardcoded paths. + +**Fix:** Use `linkTo()` or `urlFor()` helpers. + +### Step 3: Verify Routes + +Read `config/routes.cfm` and verify: +- Resource routes exist for all controllers that need them +- Root route is defined +- Route ordering is correct (resources before wildcard) +- No duplicate route definitions + +If the server is running, test key URLs: +```bash +curl -s http://localhost:PORT/ -I +curl -s http://localhost:PORT/[resource] -I +curl -s http://localhost:PORT/[resource]/1 -I +curl -s http://localhost:PORT/[resource]/new -I +``` + +Report any URLs that return 404 or 500. + +### Step 4: Verify Associations + +For each model with associations: +1. Read the model file +2. For each `hasMany`/`belongsTo`/`hasOne`: + - Verify the associated model file exists + - Verify the foreign key column exists in the migration + - Verify the reciprocal association exists (if `Post hasMany Comments`, verify `Comment belongsTo Post`) + +Report any missing reciprocal associations or foreign keys. + +### Step 5: Report Findings + +Generate a summary report: + +``` +## Validation Results + +### Test Suite +- Tests run: X +- Passed: X +- Failed: X +- [List any failures with details] + +### Anti-Pattern Scan +- Issues found: X +- [List each issue with file:line reference and description] + +### Routes +- Configured: X resource routes +- Working: X +- Broken: X +- [List any broken routes] + +### Associations +- Models checked: X +- Issues: X +- [List any missing reciprocal associations or foreign keys] + +### Overall: PASS / FAIL +[If FAIL, list the specific items that need fixing] +``` + +## What This Command Does NOT Do + +- Does not fix issues (it only reports them) +- Does not generate code +- Does not modify files + +If issues are found, the user can fix them manually or re-run `/wheels_build` after updating the spec. + +## Quick Mode + +If called with a specific scope (e.g., `/wheels_validate models`), only run the checks relevant to that scope: +- `models`: Anti-patterns 1, 9; association verification +- `controllers`: Anti-patterns 5, 6, 8; route verification +- `views`: Anti-patterns 2, 3, 10 +- `migrations`: Anti-patterns 4, 7 + +## Integration with Other Commands + +- **After /wheels_build**: Run `/wheels_validate` to verify the implementation +- **References /wheels_spec**: Can check the spec's test plan against actual test results +- **Standalone**: Can be run anytime to check codebase health diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 12de0d553b..e54d5b8b51 100755 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,15 +1,10 @@ { "permissions": { "allow": [ - "Bash(box)", "Bash(ls:*)", - "Bash(box wheels g:*)", "Bash(cd:*)", "Bash(box:*)", "Bash(rm:*)", - "mcp__puppeteer__puppeteer_navigate", - "mcp__puppeteer__puppeteer_screenshot", - "mcp__puppeteer__puppeteer_click", "Bash(find:*)", "Bash(mv:*)", "Bash(mkdir:*)", @@ -27,12 +22,8 @@ "WebFetch(domain:github.com)", "Bash(./build/scripts/debug-forgebox.sh:*)", "Bash(grep:*)", - "mcp__puppeteer__puppeteer_evaluate", - "Bash(git -C /Users/peter/projects/wheels status)", "Bash(.claude/scripts/cli/test-cli-command.sh:*)", "Bash(timeout:*)", - "Bash(/project:cli:test \"wheels init\")", - "Bash(/Users/peter/projects/wheels/test-core-commands.sh:*)", "Bash(git checkout:*)", "Bash(server start)", "Bash(gh pr view:*)", @@ -51,15 +42,7 @@ "Bash(git push:*)", "Bash(gh pr create:*)", "Bash(for:*)", - "Bash(do)", - "Bash(if [ -f \"$file\" ])", - "Bash(then)", - "Bash(echo \"EXISTS: $file\")", - "Bash(else)", "Bash(echo:*)", - "Bash(fi)", - "Bash(done)", - "mcp__puppeteer__puppeteer_fill", "Bash(curl:*)", "WebFetch(domain:raw.githubusercontent.com)", "Bash(touch:*)", @@ -75,10 +58,7 @@ "Bash(gh search commits:*)", "Bash(gh workflow:*)", "Bash(git stash:*)", - "Bash(wheels test:*)", "Bash(gh pr comment:*)", - "Bash(wheels g:*)", - "mcp__puppeteer__puppeteer_select", "Bash(wheels:*)", "Bash(time:*)", "Bash(server start openBrowser=false)", @@ -97,22 +77,14 @@ "Bash(docker:*)", "Bash(mkdocs:*)", "Bash(pip3 install:*)", - "Bash(/Users/peter/Library/Python/3.9/bin/mkdocs build)", - "Bash(/Users/peter/Library/Python/3.9/bin/mkdocs build --strict)", "Bash(sudo rm:*)", "Bash(git rm:*)", "Bash(ln:*)", - "Read(//Users/peter/projects/wheels.dev/tests/Testbox/specs/controllers/**)", "Bash(./test-ai-endpoints.sh:*)", "Bash(python3:*)", - "Read(//private/tmp/wheels-mcp-test/**)", "Bash(node:*)", "Bash(npm:*)", "WebSearch", - "Bash(WHEELS_DEV_SERVER=\"http://localhost:52905\" node mcp-server.js --test-connection)", - "Bash(export WHEELS_DEV_SERVER=\"http://localhost:52905\")", - "Read(//private/tmp/mcp-fix-test/**)", - "Read(//Users/peter/projects/ws/blog/**)", "Read(//Users/peter/projects/cfdocs/.ai/**)", "Read(//Users/peter/projects/Modern-ColdFusion-CFML-In-100-Minutes/.ai/**)", "Bash(tree:*)", @@ -120,8 +92,8 @@ "WebFetch(domain:helpx.adobe.com)", "WebFetch(domain:opencode.ai)", "Bash(lsof:*)", - "Bash(/dev/null)" + "Bash(wc:*)" ], "deny": [] } -} \ No newline at end of file +} diff --git a/.claude/skills/README.md b/.claude/skills/README.md deleted file mode 100755 index 6845d1d59f..0000000000 --- a/.claude/skills/README.md +++ /dev/null @@ -1,502 +0,0 @@ -# Wheels Claude Skills - -This directory contains **Claude Code Skills** specifically designed for Wheels framework development. Skills are specialized capabilities that Claude automatically activates based on your requests. - -## What are Skills? - -Skills are modular AI capabilities that: -- **Activate automatically** based on task context -- **Provide specialized knowledge** for specific development tasks -- **Prevent common errors** before code is written -- **Generate code following Wheels conventions** -- **Work together** to handle complex development workflows - -## Available Skills - -### Core Generator Skills - -#### 1. [wheels-model-generator](wheels-model-generator/SKILL.md) -Generate Wheels ORM models with proper validations, associations, and methods. - -**Activates when:** -- Creating new models -- Adding associations (hasMany, belongsTo, hasManyThrough) -- Adding validations -- Implementing custom model methods - -**Key Features:** -- Association patterns (one-to-many, many-to-many) -- Validation patterns (presence, uniqueness, format, length) -- Callback implementation -- Custom method templates -- Anti-pattern prevention (mixed arguments) - -#### 2. [wheels-controller-generator](wheels-controller-generator/SKILL.md) -Generate Wheels MVC controllers with CRUD actions, filters, and proper rendering. - -**Activates when:** -- Creating new controllers -- Adding CRUD actions -- Implementing filters (authentication, authorization) -- Handling form submissions -- Rendering views or JSON - -**Key Features:** -- Complete CRUD templates -- Authentication/authorization filters -- Parameter verification -- Flash messages -- API controller patterns -- Nested resource handling - -#### 3. [wheels-view-generator](wheels-view-generator/SKILL.md) -Generate Wheels view templates with proper query handling and form helpers. - -**Activates when:** -- Creating views (index, show, new, edit) -- Creating forms -- Displaying associated data -- Creating layouts or partials - -**Key Features:** -- Index view templates (list views) -- Show view templates (detail views) -- Form view templates (new/edit) -- Layout templates -- Proper query handling (prevents query/array confusion) -- Form helper usage (textField with type, not emailField) -- Association display patterns - -#### 4. [wheels-migration-generator](wheels-migration-generator/SKILL.md) -Generate database-agnostic Wheels migrations for schema changes. - -**Activates when:** -- Creating database tables -- Adding/modifying columns -- Adding indexes or foreign keys -- Changing database schema - -**Key Features:** -- Create table migrations -- Alter table migrations -- Database-agnostic date handling (prevents MySQL-specific functions) -- Join table patterns -- Index and foreign key management - -### Quality Assurance Skills - -#### 5. [wheels-anti-pattern-detector](wheels-anti-pattern-detector/SKILL.md) -**Automatically detect and prevent common Wheels errors before code is written.** - -**Activates during:** -- ANY Wheels code generation -- Model generation -- Controller generation -- View generation -- Migration generation - -**Detects:** -1. Mixed argument styles (`hasMany("comments", dependent="delete")`) -2. Query/array confusion (`ArrayLen(post.comments())`) -3. Association access in query loops -4. Non-existent form helpers (`emailField()`) -5. Rails-style nested routing -6. Database-specific SQL functions (`NOW()`, `DATE_SUB()`) -7. Missing CSRF protection -8. Inconsistent parameter styles - -**Auto-fixes all detected issues before writing files!** - -#### 6. [wheels-test-generator](wheels-test-generator/SKILL.md) -Generate TestBox BDD test specs for models, controllers, and integrations. - -**Activates when:** -- Creating tests/specs -- Testing models or controllers -- Writing integration tests - -**Key Features:** -- Model test templates (validations, associations) -- Controller test templates (actions, filters) -- Integration test templates (workflows) -- Proper setup/teardown patterns - -#### 7. [wheels-debugging](wheels-debugging/SKILL.md) -Troubleshoot common Wheels errors and provide debugging guidance. - -**Activates when:** -- Encountering errors -- Debugging issues -- Investigating unexpected behavior - -**Provides:** -- Common error solutions -- Debugging strategies -- Error message interpretation -- Fix recommendations - -### Advanced Skills - -#### 8. [wheels-refactoring](wheels-refactoring/SKILL.md) -Refactor code for better performance, security, and maintainability. - -**Activates when:** -- Optimizing code -- Fixing anti-patterns -- Improving performance -- Enhancing security - -**Patterns:** -- N+1 query elimination -- Eager loading optimization -- Security hardening -- Code quality improvements - -#### 9. [wheels-api-generator](wheels-api-generator/SKILL.md) -Generate RESTful API controllers with JSON responses and proper HTTP status codes. - -**Activates when:** -- Creating API endpoints -- Building JSON APIs -- Implementing web services - -**Features:** -- RESTful controller templates -- Proper HTTP status codes -- API authentication -- Error handling - -#### 10. [wheels-auth-generator](wheels-auth-generator/SKILL.md) -Generate authentication system with user model, sessions, and password hashing. - -**Activates when:** -- Implementing user authentication -- Creating login/logout system -- Managing sessions - -**Features:** -- User model with password hashing -- Sessions controller -- Authentication filters -- Secure password handling - -#### 11. [wheels-deployment](wheels-deployment/SKILL.md) -Configure applications for production deployment with security and performance. - -**Activates when:** -- Preparing for production -- Configuring servers -- Hardening security - -**Provides:** -- Production configuration -- Security checklist -- Performance optimization -- Environment settings - -#### 12. [wheels-documentation-generator](wheels-documentation-generator/SKILL.md) -Generate documentation comments, READMEs, and API documentation. - -**Activates when:** -- Documenting code -- Creating READMEs -- Generating API docs - -**Features:** -- Function documentation templates -- Model documentation -- README templates -- API documentation - -#### 13. [wheels-plugin-generator](wheels-plugin-generator/SKILL.md) -Generate Wheels plugins with proper structure and ForgeBox packaging. - -**Activates when:** -- Creating plugins -- Extending Wheels functionality -- Packaging reusable components - -**Features:** -- Plugin directory structure -- Plugin configuration -- ForgeBox packaging -- Event handlers -- Mixin methods - -#### 14. [wheels-email-generator](wheels-email-generator/SKILL.md) -Generate email functionality including mailers, templates, and configuration. - -**Activates when:** -- Sending emails -- Creating notifications -- Implementing transactional emails - -**Features:** -- Mailer controllers -- Email templates (HTML/text) -- Email layouts -- SMTP configuration -- Attachment handling - -#### 15. [wheels-routing-generator](wheels-routing-generator/SKILL.md) -Generate RESTful routes, nested routes, and custom routing patterns. - -**Activates when:** -- Creating routes -- Defining URL structure -- Implementing RESTful resources - -**Features:** -- RESTful resource routes -- Nested routing -- Route constraints -- API versioning -- Named routes -- Route namespacing - -## How Skills Work - -### Automatic Activation - -Skills activate automatically when Claude detects relevant keywords or task patterns: - -``` -User: "Create a Post model with comments association" - -Claude: (automatically activates wheels-model-generator) - (automatically activates wheels-anti-pattern-detector) - → Generates model with correct patterns - → Validates code before writing - → Prevents mixed argument styles -``` - -### Skill Composition - -Multiple skills work together on complex tasks: - -``` -User: "Create a blog with posts and comments" - -Claude activates: -1. wheels-model-generator (Post and Comment models) -2. wheels-migration-generator (database tables) -3. wheels-controller-generator (Posts and Comments controllers) -4. wheels-view-generator (index, show, new, edit views) -5. wheels-anti-pattern-detector (validates all generated code) -6. wheels-test-generator (creates test specs) - -Result: Complete, tested, validated blog feature! -``` - -### Validation Workflow - -The anti-pattern-detector skill runs automatically during code generation: - -``` -1. Generate code (model/controller/view/migration) -2. Scan for anti-patterns -3. If found: - - Display warning - - Show before/after - - Auto-fix -4. Write validated code to file -``` - -## Benefits Over .ai Folder Approach - -| Feature | .ai Folder | Claude Skills | -|---------|------------|---------------| -| **Activation** | Manual loading | Automatic | -| **Token Usage** | 5,000-10,000 per task | 500-1,500 per task | -| **Specialization** | Generic docs | Task-specific | -| **Validation** | Manual | Automatic before file write | -| **Composition** | Sequential | Parallel & composable | -| **User Experience** | Slow, manual | Fast, seamless | - -## Token Efficiency - -**Example Task: Generate Post Model** - -**With .ai folder:** -``` -1. Read .ai/wheels/models/architecture.md (2,000 tokens) -2. Read .ai/wheels/models/associations.md (1,500 tokens) -3. Read .ai/wheels/models/validations.md (1,800 tokens) -4. Read .ai/wheels/troubleshooting/common-errors.md (1,200 tokens) -Total: 6,500 tokens -``` - -**With Skills:** -``` -1. Activate wheels-model-generator (800 tokens) -2. Activate wheels-anti-pattern-detector (400 tokens) -Total: 1,200 tokens -``` - -**Result: 81% reduction in token usage!** - -## Usage Examples - -### Create a Model - -``` -User: "Create a User model with email and password validation" - -Activated Skills: -- wheels-model-generator -- wheels-anti-pattern-detector - -Generated: -- User.cfc with proper validations -- All validations use named parameters -- No anti-patterns present -``` - -### Create a CRUD Controller - -``` -User: "Create a Posts controller with full CRUD" - -Activated Skills: -- wheels-controller-generator -- wheels-anti-pattern-detector - -Generated: -- Posts.cfc with index, show, new, create, edit, update, delete -- Parameter verification configured -- Filters for findPost -- Flash messages -- Proper redirects -``` - -### Create Views - -``` -User: "Create views for the Posts controller" - -Activated Skills: -- wheels-view-generator -- wheels-anti-pattern-detector - -Generated: -- index.cfm (proper query loops) -- show.cfm (association display) -- new.cfm (form with validation errors) -- edit.cfm (pre-populated form) -- All use textField with type (not emailField) -- All forms include CSRF protection -``` - -### Complete Feature - -``` -User: "Create a blog with posts and comments" - -Activated Skills: -- wheels-model-generator (x2 for Post and Comment) -- wheels-migration-generator (x2 for tables) -- wheels-controller-generator (x2 for controllers) -- wheels-view-generator (x8 for all views) -- wheels-anti-pattern-detector (validates everything) - -Result: Complete blog feature with zero anti-patterns! -``` - -## Skill Development - -### Creating New Skills - -See [CLAUDE-SKILLS-ANALYSIS.md](../../CLAUDE-SKILLS-ANALYSIS.md) for: -- Skill architecture guidelines -- Template structure -- YAML frontmatter format -- Best practices -- Integration patterns - -### Skill Template - -```markdown ---- -name: Skill Name -description: What the skill does and when Claude should activate it. ---- - -# Skill Name - -## When to Use This Skill - -Activate when: -- User requests X -- User mentions Y -- User is doing Z - -## Templates - -[Templates here] - -## Patterns - -[Patterns here] - -## Related Skills - -- skill-1 -- skill-2 -``` - -## Migration from .ai Folder - -**Skills complement the .ai folder, not replace it:** - -- **.ai folder**: Comprehensive reference documentation for humans and AI -- **Skills**: Focused, automatic, task-specific generation - -Both serve different but complementary purposes. - -## Testing Skills - -To verify skills are working: - -1. Request a common task (e.g., "create a User model") -2. Observe which skills activate (Claude will mention them) -3. Check generated code for proper patterns -4. Verify anti-patterns were prevented - -## Troubleshooting - -### Skill Not Activating - -- Check skill description matches your request keywords -- Use more specific language (e.g., "create model" vs "make user thing") -- Mention skill explicitly if needed - -### Wrong Pattern Generated - -- Skill may need description refinement -- Report issue for skill improvement -- Use anti-pattern-detector to validate - -## Contributing - -To improve or add skills: - -1. Follow skill template structure -2. Include comprehensive description for activation -3. Provide clear examples -4. Add anti-pattern rules -5. Test with various requests -6. Document in this README - -## Version History - -- **v1.0** (2025-10-20): Initial skill implementation - - 12 core skills - - Automatic anti-pattern detection - - Comprehensive code generation - - Token-efficient operation - ---- - -**Skills are the future of AI-assisted Wheels development!** - -For questions or suggestions, see [CLAUDE-SKILLS-ANALYSIS.md](../../CLAUDE-SKILLS-ANALYSIS.md). diff --git a/.claude/skills/SKILLS-QUICK-START.md b/.claude/skills/SKILLS-QUICK-START.md deleted file mode 100755 index d20428625b..0000000000 --- a/.claude/skills/SKILLS-QUICK-START.md +++ /dev/null @@ -1,104 +0,0 @@ -# Wheels Claude Skills - Quick Start Guide - -Get started with Wheels Claude Skills in 60 seconds! - -## What Are Skills? - -Skills are **automatic AI assistants** that activate when you need them. Just describe what you want, and the right skills activate automatically. - -## Try It Now - -### 1. Create a Model - -``` -You: "Create a User model with email and password" - -Skills activate automatically: -- wheels-model-generator -- wheels-anti-pattern-detector - -Result: User.cfc with proper validations, no errors! -``` - -### 2. Create a Controller - -``` -You: "Create a Posts controller with CRUD actions" - -Skills activate automatically: -- wheels-controller-generator -- wheels-anti-pattern-detector - -Result: Complete CRUD controller with filters! -``` - -### 3. Create Views - -``` -You: "Create views for Posts controller" - -Skills activate automatically: -- wheels-view-generator -- wheels-anti-pattern-detector - -Result: index, show, new, edit views with proper patterns! -``` - -### 4. Complete Feature - -``` -You: "Create a blog with posts and comments" - -Skills activate automatically: -- ALL relevant skills -- Generates models, migrations, controllers, views -- Validates everything -- Creates tests - -Result: Complete blog in 3 minutes! -``` - -## Available Skills - -1. **wheels-model-generator** - Models with associations/validations -2. **wheels-controller-generator** - CRUD controllers with filters -3. **wheels-view-generator** - Views with proper query handling -4. **wheels-migration-generator** - Database-agnostic migrations -5. **wheels-anti-pattern-detector** - Automatic code validation (ALWAYS ON) -6. **wheels-test-generator** - TestBox BDD specs -7. **wheels-debugging** - Error troubleshooting -8. **wheels-refactoring** - Performance optimization -9. **wheels-api-generator** - RESTful JSON APIs -10. **wheels-auth-generator** - User authentication -11. **wheels-deployment** - Production configuration -12. **wheels-documentation-generator** - Code documentation - -## Key Benefits - -✅ **Automatic** - No manual activation needed -✅ **Fast** - 5-10x faster than manual coding -✅ **Error-Free** - Anti-patterns prevented automatically -✅ **Smart** - Skills work together intelligently -✅ **Efficient** - 70-80% less tokens used - -## How It Works - -1. You describe what you need -2. Claude automatically selects relevant skills -3. Skills generate code with correct patterns -4. Anti-pattern detector validates before writing -5. Perfect code written to files - -## No Configuration Needed - -Skills work immediately - just start using Claude Code in your Wheels project! - -## Learn More - -- **Full Guide:** [README.md](README.md) -- **Analysis:** [../../CLAUDE-SKILLS-ANALYSIS.md](../../CLAUDE-SKILLS-ANALYSIS.md) -- **Summary:** [../../SKILLS-IMPLEMENTATION-SUMMARY.md](../../SKILLS-IMPLEMENTATION-SUMMARY.md) - ---- - -**Ready? Just ask Claude to create something and watch the magic happen!** ✨ diff --git a/.claude/skills/wheels-anti-pattern-detector/SKILL.md b/.claude/skills/wheels-anti-pattern-detector/SKILL.md deleted file mode 100755 index 02e71faf2e..0000000000 --- a/.claude/skills/wheels-anti-pattern-detector/SKILL.md +++ /dev/null @@ -1,576 +0,0 @@ ---- -name: Wheels Anti-Pattern Detector -description: Automatically detect and prevent common Wheels framework errors before code is generated. This skill activates during ANY Wheels code generation (models, controllers, views, migrations) to validate patterns and prevent known issues. Scans for mixed arguments, query/array confusion, non-existent helpers, and database-specific SQL. ---- - -# Wheels Anti-Pattern Detector - -## Purpose - -This skill runs **AUTOMATICALLY** during any Wheels code generation to catch common errors before they're written to files. - -## When to Use This Skill - -Activate automatically during: -- Model generation (check associations, validations) -- Controller generation (check method calls) -- View generation (check queries, form helpers) -- Migration generation (check SQL compatibility) -- Code review and refactoring -- Any Wheels code modification - -## 🚨 Production-Tested Critical Detections - -### 1. CLI Generator String Boolean Values (CRITICAL) - -**🔴 CRITICAL:** The CLI `wheels g migration` command generates migrations with string boolean values that silently fail. - -**Detection Pattern:** -```regex -createTable\s*\([^)]*force\s*=\s*['"][^'"]*['"] -createTable\s*\([^)]*id\s*=\s*['"][^'"]*['"] -``` - -**Examples to Detect:** -```cfm -❌ t = createTable(name='users', force='false', id='true', primaryKey='id'); -❌ t = createTable(name='posts', force='false'); -❌ t = createTable(name='comments', id='true'); -``` - -**Auto-Fix:** -```cfm -✅ t = createTable(name='users'); -✅ t = createTable(name='posts'); -✅ t = createTable(name='comments'); -``` - -**Error Message:** -``` -⚠️ CRITICAL: CLI-generated string boolean values detected -Line: 5 -Found: createTable(name='users', force='false', id='true', primaryKey='id') -Fix: createTable(name='users') - -CLI generators create STRING booleans ('false', 'true') that don't work. -Remove force/id/primaryKey parameters and use Wheels defaults instead. -This will cause "NoPrimaryKey" errors if not fixed! -``` - -### 2. Missing setPrimaryKey() in Models (CRITICAL) - -**🔴 CRITICAL:** Models must explicitly call `setPrimaryKey("id")` in config(), even when migrations are correct. - -**Detection Pattern:** -```regex -component\s+extends\s*=\s*["']Model["'][\s\S]*?function\s+config\s*\(\s*\)\s*\{(?![\s\S]*?setPrimaryKey) -``` - -**Examples to Detect:** -```cfm -❌ component extends="Model" { - function config() { - table("users"); - hasMany(name="posts"); // Missing setPrimaryKey! - } -} -``` - -**Auto-Fix:** -```cfm -✅ component extends="Model" { - function config() { - table("users"); - setPrimaryKey("id"); // Added! - hasMany(name="posts"); - } -} -``` - -**Error Message:** -``` -⚠️ CRITICAL: Missing setPrimaryKey() in model config() -Line: 3 -Found: config() without setPrimaryKey() declaration -Fix: Add setPrimaryKey("id") as first line after table() declaration - -Even with correct migrations, Wheels ORM requires explicit primary key declaration. -This will cause "Wheels.NoPrimaryKey" errors if not added! -``` - -### 3. Property Access Without structKeyExists() Check (CRITICAL) - -**🔴 CRITICAL:** Accessing properties in beforeCreate/beforeValidation callbacks without existence check causes errors. - -**Detection Pattern:** -```regex -function\s+(beforeCreate|beforeValidation|setDefaults)[\s\S]*?if\s*\(\s*!len\s*\(\s*this\.\w+\s*\)\s*\)(?![\s\S]*?structKeyExists) -``` - -**Examples to Detect:** -```cfm -❌ function setDefaults() { - if (!len(this.followersCount)) { // Error if property doesn't exist! - this.followersCount = 0; - } -} -``` - -**Auto-Fix:** -```cfm -✅ function setDefaults() { - if (!structKeyExists(this, "followersCount") || !len(this.followersCount)) { - this.followersCount = 0; - } -} -``` - -**Error Message:** -``` -⚠️ CRITICAL: Property access without structKeyExists() check -Line: 15 -Found: if (!len(this.followersCount)) in beforeCreate callback -Fix: if (!structKeyExists(this, "followersCount") || !len(this.followersCount)) - -In beforeCreate/beforeValidation callbacks, properties may not exist yet. -This will cause "no accessible Member" errors if not checked! -``` - -### 4. Wrong Validation Parameter Names (CRITICAL) - -**🔴 CRITICAL:** Validation functions use "properties" (plural) not "property" (singular). - -**Detection Pattern:** -```regex -validates\w+Of\s*\(\s*property\s*= -``` - -**Examples to Detect:** -```cfm -❌ validatesPresenceOf(property="username,email") -❌ validatesUniquenessOf(property="email") -❌ validatesFormatOf(property="email", regEx="...") -``` - -**Auto-Fix:** -```cfm -✅ validatesPresenceOf(properties="username,email") -✅ validatesUniquenessOf(properties="email") -✅ validatesFormatOf(properties="email", regEx="...") -``` - -**Error Message:** -``` -⚠️ CRITICAL: Wrong validation parameter name -Line: 8 -Found: validatesPresenceOf(property="username") -Fix: validatesPresenceOf(properties="username") - -Wheels validation functions use "properties" (PLURAL), not "property". -This validation will be silently ignored if not fixed! -``` - -## Critical Anti-Patterns - -### 5. Mixed Argument Styles - -**Detection Pattern:** -```regex -(hasMany|belongsTo|hasManyThrough|validatesPresenceOf|validatesUniquenessOf|validatesFormatOf|validatesLengthOf|findByKey|findAll|findOne)\s*\(\s*"[^"]+"\s*,\s*\w+\s*= -``` - -**Examples:** -```cfm -❌ hasMany("comments", dependent="delete") -❌ belongsTo("user", foreignKey="userId") -❌ validatesPresenceOf("title", message="Required") -❌ findByKey(params.key, include="comments") -❌ findAll(order="id DESC", where="active = 1") -``` - -**Auto-Fix:** -```cfm -✅ hasMany(name="comments", dependent="delete") -✅ belongsTo(name="user", foreignKey="userId") -✅ validatesPresenceOf(property="title", message="Required") -✅ findByKey(key=params.key, include="comments") -✅ findAll(order="id DESC", where="active = 1") // No positional args, OK -``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: Mixed argument styles -Line: 5 -Found: hasMany("comments", dependent="delete") -Fix: hasMany(name="comments", dependent="delete") - -Wheels requires consistent parameter syntax - either ALL positional OR ALL named. -When using options like 'dependent', you MUST use named arguments for ALL parameters. -``` - -### 2. Query/Array Confusion - -**Detection Pattern:** -```regex -ArrayLen\s*\(\s*\w+\.(comments|posts|tags|users|[a-z]+)\(\s*\)\s*\) -``` - -**Examples:** -```cfm -❌ -❌ -❌ -``` - -**Auto-Fix:** -```cfm -✅ -✅ // After: comments = post.comments() -✅ -``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: ArrayLen() on query object -Line: 12 -Found: ArrayLen(post.comments()) -Fix: post.comments().recordCount - -Wheels associations return QUERIES, not arrays. Use .recordCount for count. -``` - -### 3. Association Access Inside Query Loops - -**Detection Pattern:** -```regex -[\s\S]*?\.\w+\(\)\.recordCount -``` - -**Examples:** -```cfm -❌ -

#posts.comments().recordCount# comments

-
-``` - -**Auto-Fix:** -```cfm -✅ - -

#postComments.recordCount# comments

-
-``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: Association access inside query loop -Line: 15 -Found: posts.comments().recordCount inside -Fix: Load association separately: postComments = model("Post").findByKey(posts.id).comments() - -Cannot access associations directly on query objects inside loops. -Must reload the model object first. -``` - -### 4. Non-Existent Form Helpers - -**Detection Pattern:** -```regex -(emailField|passwordField|numberField|dateField|timeField|urlField|telField)\s*\( -``` - -**Examples:** -```cfm -❌ #emailField(objectName="user", property="email")# -❌ #passwordField(objectName="user", property="password")# -❌ #numberField(objectName="product", property="price")# -❌ #urlField(objectName="company", property="website")# -``` - -**Auto-Fix:** -```cfm -✅ #textField(objectName="user", property="email", type="email")# -✅ #textField(objectName="user", property="password", type="password")# -✅ #textField(objectName="product", property="price", type="number")# -✅ #textField(objectName="company", property="website", type="url")# -``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: Non-existent form helper -Line: 23 -Found: emailField(objectName="user", property="email") -Fix: textField(objectName="user", property="email", type="email") - -Wheels doesn't have specialized field helpers like emailField(). -Use textField() with the 'type' attribute instead. -``` - -### 5. Rails-Style Nested Routing - -**Detection Pattern:** -```regex -resources\s*\([^)]+,\s*(nested|namespace)\s*= -``` - -**Examples:** -```cfm -❌ resources("posts", nested=resources("comments")) -❌ resources("users", namespace="admin") -``` - -**Auto-Fix:** -```cfm -✅ resources("posts") -✅ resources("comments") -// Define separately, not nested -``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: Rails-style nested routing -Line: 8 -Found: resources("posts", nested=resources("comments")) -Fix: resources("posts") and resources("comments") as separate declarations - -Wheels doesn't support Rails-style nested resources. -Define resources separately and handle nesting in controllers. -``` - -### 6. Database-Specific SQL Functions - -**Detection Pattern:** -```regex -(DATE_SUB|DATE_ADD|NOW|CURDATE|CURTIME|DATEDIFF|INTERVAL)\s*\( -``` - -**Examples:** -```cfm -❌ execute("INSERT INTO posts (publishedAt) VALUES (DATE_SUB(NOW(), INTERVAL 1 DAY))") -❌ execute("SELECT * FROM posts WHERE createdAt > CURDATE()") -❌ execute("UPDATE posts SET modifiedAt = NOW()") -``` - -**Auto-Fix:** -```cfm -✅ var pastDate = DateAdd("d", -1, Now()); - execute("INSERT INTO posts (publishedAt) VALUES (TIMESTAMP '#DateFormat(pastDate, "yyyy-mm-dd")# #TimeFormat(pastDate, "HH:mm:ss")#')") - -✅ var today = Now(); - execute("SELECT * FROM posts WHERE createdAt > TIMESTAMP '#DateFormat(today, "yyyy-mm-dd")# 00:00:00'") - -✅ var now = Now(); - execute("UPDATE posts SET modifiedAt = TIMESTAMP '#DateFormat(now, "yyyy-mm-dd")# #TimeFormat(now, "HH:mm:ss")#'") -``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: Database-specific SQL function -Line: 34 -Found: DATE_SUB(NOW(), INTERVAL 1 DAY) -Fix: Use CFML DateAdd() + TIMESTAMP formatting - -MySQL-specific date functions won't work across all databases. -Use CFML date functions (DateAdd, DateFormat, TimeFormat) for compatibility. -``` - -### 7. Missing CSRF Protection Check - -**Detection Pattern:** -```regex -]*method\s*=\s*["']post["'][^>]*>(?![\s\S]*csrf) -``` - -**Examples:** -```cfm -❌
- -
-``` - -**Auto-Fix:** -```cfm -✅ #startFormTag(action="create", method="post")# - -#endFormTag()# -``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: Form without CSRF protection -Line: 45 -Found:
without CSRF token -Fix: Use #startFormTag()# which includes CSRF automatically - -Wheels provides built-in CSRF protection. -Use startFormTag() instead of raw tags. -``` - -### 8. Inconsistent Property Style in config() - -**Detection Pattern:** -Check for mixing positional and named arguments within same config() function - -**Examples:** -```cfm -❌ function config() { - hasMany("comments"); // Positional - belongsTo(name="user"); // Named -} -``` - -**Auto-Fix:** -```cfm -✅ function config() { - hasMany(name="comments"); // All named - belongsTo(name="user"); // All named -} -``` - -**Error Message:** -``` -⚠️ ANTI-PATTERN DETECTED: Inconsistent parameter style in config() -Lines: 5-6 -Found: Mixed positional and named arguments -Fix: Use SAME style for ALL association/validation declarations - -config() function should use consistent argument style throughout. -Either ALL positional OR ALL named - never mix them. -``` - -## Validation Workflow - -### Before Writing Any File - -1. **Scan Generated Code:** Run all anti-pattern regex checks -2. **If Pattern Detected:** - - Display warning message - - Show before/after comparison - - Auto-fix the code - - Log the fix for user awareness -3. **Validate Fix:** Ensure fix doesn't introduce new issues -4. **Write Corrected File:** Save the validated code - -### Example Validation Output - -``` -🔍 Validating generated code... - -⚠️ 3 anti-patterns detected and auto-fixed: - -1. [Line 8] Mixed argument styles - Before: hasMany("comments", dependent="delete") - After: hasMany(name="comments", dependent="delete") - -2. [Line 15] Query/Array confusion - Before: ArrayLen(post.comments()) - After: post.comments().recordCount - -3. [Line 23] Non-existent helper - Before: emailField(objectName="user", property="email") - After: textField(objectName="user", property="email", type="email") - -✅ All anti-patterns fixed. Writing file... -``` - -## Integration Points - -### Auto-Activation During: - -1. **Model Generation** (wheels-model-generator) - - Check association argument styles - - Check validation argument styles - - Check callback definitions - -2. **Controller Generation** (wheels-controller-generator) - - Check findByKey/findAll argument styles - - Check renderPage/redirectTo calls - - Check parameter verification - -3. **View Generation** (wheels-view-generator) - - Check query handling - - Check form helper usage - - Check association access in loops - - Check CSRF protection - -4. **Migration Generation** (wheels-migration-generator) - - Check for database-specific SQL - - Check date/time handling - - Check transaction structure - -## Testing Anti-Pattern Detection - -### Test Cases - -```cfm -// Test Case 1: Should detect mixed arguments -Input: hasMany("comments", dependent="delete") -Detect: ✅ YES -Fix: hasMany(name="comments", dependent="delete") - -// Test Case 2: Should allow consistent named arguments -Input: hasMany(name="comments", dependent="delete") -Detect: ❌ NO (Correct pattern) - -// Test Case 3: Should allow consistent positional (no options) -Input: hasMany("comments") -Detect: ❌ NO (Correct pattern) - -// Test Case 4: Should detect ArrayLen on association -Input: ArrayLen(post.comments()) -Detect: ✅ YES -Fix: post.comments().recordCount - -// Test Case 5: Should detect non-existent helper -Input: emailField(objectName="user", property="email") -Detect: ✅ YES -Fix: textField(objectName="user", property="email", type="email") - -// Test Case 6: Should detect database-specific SQL -Input: "INSERT INTO posts (date) VALUES (NOW())" -Detect: ✅ YES -Fix: Use CFML Now() with formatting - -// Test Case 7: Should detect inconsistent config styles -Input: hasMany("comments") + belongsTo(name="user") -Detect: ✅ YES -Fix: Both use named arguments -``` - -## Configuration - -### Enable/Disable Checks - -```json -// .claude/anti-pattern-config.json -{ - "checks": { - "mixedArguments": true, - "queryArrayConfusion": true, - "nonExistentHelpers": true, - "railsRouting": true, - "databaseSpecificSQL": true, - "csrfProtection": true, - "inconsistentConfig": true - }, - "autoFix": true, - "reportLevel": "warning" -} -``` - -## Success Metrics - -- **Detection Rate:** 100% of known anti-patterns caught -- **False Positives:** <5% -- **Auto-Fix Success:** >95% -- **User Awareness:** Clear before/after shown for all fixes - -## Related Skills - -All Wheels generator skills depend on this skill for validation. - ---- - -**Generated by:** Wheels Anti-Pattern Detector Skill v1.0 -**Framework:** CFWheels 3.0+ -**Last Updated:** 2025-10-20 diff --git a/.claude/skills/wheels-api-generator/SKILL.md b/.claude/skills/wheels-api-generator/SKILL.md deleted file mode 100755 index d8c0a8e9f3..0000000000 --- a/.claude/skills/wheels-api-generator/SKILL.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -name: Wheels API Generator -description: Generate RESTful API controllers with JSON responses, proper HTTP status codes, and API authentication. Use when creating API endpoints, JSON APIs, or web services. Ensures proper REST conventions and error handling. ---- - -# Wheels API Generator - -## When to Use This Skill - -Activate when: -- User requests to create an API -- User wants JSON endpoints -- User mentions: API, REST, JSON, endpoint, web service - -## API Controller Template - -```cfm -component extends="Controller" { - - function config() { - provides("json"); - verifies(only="show,update,delete", params="key", paramsTypes="integer"); - filters(through="requireApiAuth"); - } - - function index() { - resources = model("Resource").findAll(order="createdAt DESC"); - - renderWith( - data=resources, - format="json", - status=200 - ); - } - - function show() { - resource = model("Resource").findByKey(key=params.key); - - if (!isObject(resource)) { - renderWith( - data={error="Resource not found"}, - format="json", - status=404 - ); - return; - } - - renderWith( - data=resource, - format="json", - status=200 - ); - } - - function create() { - resource = model("Resource").new(params.resource); - - if (resource.save()) { - renderWith( - data=resource, - format="json", - status=201, - location=urlFor(action="show", key=resource.key()) - ); - } else { - renderWith( - data={errors=resource.allErrors()}, - format="json", - status=422 - ); - } - } - - function update() { - resource = model("Resource").findByKey(key=params.key); - - if (!isObject(resource)) { - renderWith(data={error="Not found"}, format="json", status=404); - return; - } - - if (resource.update(params.resource)) { - renderWith(data=resource, format="json", status=200); - } else { - renderWith(data={errors=resource.allErrors()}, format="json", status=422); - } - } - - function delete() { - resource = model("Resource").findByKey(key=params.key); - - if (!isObject(resource)) { - renderWith(data={error="Not found"}, format="json", status=404); - return; - } - - resource.delete(); - renderWith(data={message="Deleted"}, format="json", status=204); - } - - private function requireApiAuth() { - var headers = getHTTPRequestData().headers; - - if (!structKeyExists(headers, "Authorization")) { - renderWith(data={error="Unauthorized"}, format="json", status=401); - abort; - } - - // Validate API token - var token = replace(headers.Authorization, "Bearer ", ""); - if (!isValidApiToken(token)) { - renderWith(data={error="Invalid token"}, format="json", status=401); - abort; - } - } - - private boolean function isValidApiToken(required string token) { - // Token validation logic - return true; - } -} -``` - -## HTTP Status Codes - -- **200 OK**: Successful GET, PUT, PATCH -- **201 Created**: Successful POST -- **204 No Content**: Successful DELETE -- **400 Bad Request**: Invalid request data -- **401 Unauthorized**: Missing/invalid authentication -- **403 Forbidden**: Insufficient permissions -- **404 Not Found**: Resource doesn't exist -- **422 Unprocessable Entity**: Validation errors -- **500 Internal Server Error**: Server error - ---- - -**Generated by:** Wheels API Generator Skill v1.0 diff --git a/.claude/skills/wheels-auth-generator/SKILL.md b/.claude/skills/wheels-auth-generator/SKILL.md deleted file mode 100755 index ac12c1cee3..0000000000 --- a/.claude/skills/wheels-auth-generator/SKILL.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -name: Wheels Auth Generator -description: Generate authentication system with user model, sessions controller, and password hashing. Use when implementing user authentication, login/logout, or session management. Provides secure authentication patterns and bcrypt support. ---- - -# Wheels Auth Generator - -## When to Use This Skill - -Activate when: -- User requests authentication/login system -- User wants user registration -- User mentions: auth, login, logout, session, password, signup - -## User Model with Authentication - -```cfm -component extends="Model" { - - function config() { - validatesPresenceOf(property="email,password"); - validatesUniquenessOf(property="email"); - validatesFormatOf( - property="email", - regEx="^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$" - ); - validatesLengthOf(property="password", minimum=8); - validatesConfirmationOf(property="password"); - - beforeSave("hashPassword"); - } - - private function hashPassword() { - if (structKeyExists(this, "password") && len(this.password) && !isHashed(this.password)) { - this.password = hash(this.password, "SHA-512"); - } - } - - private boolean function isHashed(required string password) { - return len(arguments.password) == 128; - } - - public any function authenticate(required string email, required string password) { - var user = this.findOne(where="email = '#arguments.email#'"); - - if (!isObject(user)) return false; - - var hashedAttempt = hash(arguments.password, "SHA-512"); - - return (user.password == hashedAttempt) ? user : false; - } -} -``` - -## Sessions Controller - -```cfm -component extends="Controller" { - - function new() { - // Show login form - } - - function create() { - var user = model("User").authenticate( - email=params.email, - password=params.password - ); - - if (isObject(user)) { - session.userId = user.id; - flashInsert(success="Welcome back!"); - redirectTo(controller="home", action="index"); - } else { - flashInsert(error="Invalid email or password"); - renderPage(action="new"); - } - } - - function delete() { - structDelete(session, "userId"); - flashInsert(success="You have been logged out"); - redirectTo(controller="home", action="index"); - } -} -``` - -## Authentication Filter - -```cfm -// In any controller requiring authentication -function config() { - filters(through="requireAuth"); -} - -private function requireAuth() { - if (!structKeyExists(session, "userId")) { - flashInsert(error="Please log in"); - redirectTo(controller="sessions", action="new"); - } -} -``` - -## Password Reset (Security Best Practices) - -### PasswordResets Controller - -```cfm -component extends="Controller" { - - /** - * Process password reset request - * SECURITY: Always show same message regardless of email existence - */ - function create() { - if (!structKeyExists(params, "email") || !len(trim(params.email))) { - flashInsert(error="Please provide your email address."); - renderView(action="new"); - return; - } - - // Find user by email - user = model("User").findOne(where="email = '#params.email#' AND deletedAt IS NULL"); - - // CRITICAL: Always show success message (prevents email enumeration) - if (isObject(user)) { - user.generateResetToken(); - user.save(); - // TODO: Send email with reset link - } - - // Same message whether email exists or not - flashInsert(success="If that email address is in our system, we've sent password reset instructions."); - redirectTo(controller="sessions", action="new"); - } - - /** - * Show password reset form - validates token - */ - function edit() { - if (!structKeyExists(params, "token") || !len(trim(params.token))) { - flashInsert(error="Invalid password reset link."); - redirectTo(controller="sessions", action="new"); - return; - } - - user = model("User").findOne(where="resetToken = '#params.token#' AND deletedAt IS NULL"); - - if (!isObject(user)) { - flashInsert(error="Invalid or expired password reset link."); - redirectTo(controller="sessions", action="new"); - return; - } - - // Check token expiry (1 hour) - if (!user.isResetTokenValid()) { - flashInsert(error="This password reset link has expired. Please request a new one."); - redirectTo(controller="passwordResets", action="new"); - return; - } - - token = params.token; - } - - /** - * Process password reset - single-use token - */ - function update() { - // Validate token and update password - user = model("User").findOne(where="resetToken = '#params.token#' AND deletedAt IS NULL"); - - if (!isObject(user) || !user.isResetTokenValid()) { - flashInsert(error="Invalid or expired password reset link."); - redirectTo(controller="sessions", action="new"); - return; - } - - user.password = params.password; - user.passwordConfirmation = params.passwordConfirmation; - user.clearResetToken(); // Single-use token - - if (user.save()) { - // Auto-login after successful reset - session.userId = user.id; - session.userEmail = user.email; - flashInsert(success="Your password has been reset successfully!"); - redirectTo(controller="home", action="index"); - } - } -} -``` - -### User Model Token Methods - -```cfm -// Add to User model config() -function config() { - // ... other config ... - beforeCreate("setDefaults"); -} - -/** - * Generate password reset token (1-hour expiry) - */ -public void function generateResetToken() { - this.resetToken = hash(createUUID() & now(), "SHA-256"); - this.resetTokenExpiry = dateAdd("h", 1, now()); -} - -/** - * Check if reset token is valid (not expired) - */ -public boolean function isResetTokenValid() { - if (!structKeyExists(this, "resetToken") || !len(this.resetToken)) { - return false; - } - if (!structKeyExists(this, "resetTokenExpiry")) { - return false; - } - return dateCompare(now(), this.resetTokenExpiry) < 0; -} - -/** - * Clear reset token after use (single-use) - */ -public void function clearResetToken() { - this.resetToken = ""; - this.resetTokenExpiry = ""; -} -``` - -### Security Checklist for Password Reset - -- ✅ **Email enumeration prevention**: Always show same success message -- ✅ **Token expiry**: Limit token validity (1 hour recommended) -- ✅ **Single-use tokens**: Clear token after successful reset -- ✅ **Auto-login**: Log user in after successful reset for better UX -- ✅ **Secure token generation**: Use cryptographic hash with UUID + timestamp -- ✅ **HTTPS only**: Password reset links should only work over HTTPS in production - ---- - -**Generated by:** Wheels Auth Generator Skill v1.0 diff --git a/.claude/skills/wheels-codegen/SKILL.md b/.claude/skills/wheels-codegen/SKILL.md new file mode 100644 index 0000000000..927f5e30d6 --- /dev/null +++ b/.claude/skills/wheels-codegen/SKILL.md @@ -0,0 +1,739 @@ +--- +name: Wheels Code Generator +description: Generate and validate Wheels MVC components — models, controllers, views, migrations, and routes. Detects anti-patterns (mixed args, query/array confusion, missing setPrimaryKey, wrong validation params, non-existent helpers, database-specific SQL). Use when creating any Wheels component, modifying schema, defining routes, or reviewing generated code for correctness. +--- + +# Wheels Code Generator + +Generates models, controllers, views, migrations, and routes for CFWheels 3.0+ applications. Includes inline anti-pattern detection to catch common errors before they reach production. + +## Anti-Pattern Rules + +These rules apply to ALL generated Wheels code. Check every file before writing. + +### 1. Never Mix Positional and Named Arguments + +```cfm +hasMany("comments", dependent="delete") // WRONG +hasMany(name="comments", dependent="delete") // CORRECT +belongsTo("user", foreignKey="userId") // WRONG +belongsTo(name="user", foreignKey="userId") // CORRECT +validatesPresenceOf("title", message="Required") // WRONG +findByKey(params.key, include="comments") // WRONG +findByKey(key=params.key, include="comments") // CORRECT +``` + +Use the SAME style throughout the entire config() function. If any call needs named params, use named params for ALL calls. + +### 2. Validation Parameter Is "properties" (Plural) + +```cfm +validatesPresenceOf(property="username,email") // WRONG - silently ignored +validatesPresenceOf(properties="username,email") // CORRECT +validatesUniquenessOf(properties="email") // CORRECT +validatesFormatOf(properties="email", regEx="...") // CORRECT +validatesLengthOf(properties="username", minimum=3)// CORRECT +validate(methods="customValidation") // CORRECT ("methods" plural) +``` + +### 3. Always Add setPrimaryKey() to Models + +Even when migrations correctly create the primary key, models MUST declare it explicitly: + +```cfm +component extends="Model" { + function config() { + table("users"); + setPrimaryKey("id"); // MANDATORY - causes NoPrimaryKey error if missing + hasMany(name="posts"); + } +} +``` + +### 4. structKeyExists() Before Property Access in Callbacks + +In beforeCreate/beforeValidation callbacks, properties may not exist yet: + +```cfm +// WRONG - throws "no accessible Member" error +if (!len(this.followersCount)) { this.followersCount = 0; } + +// CORRECT +if (!structKeyExists(this, "followersCount") || !len(this.followersCount)) { + this.followersCount = 0; +} +``` + +### 5. Associations Return Queries, Not Arrays + +```cfm +ArrayLen(post.comments()) // WRONG +post.comments().recordCount // CORRECT + // WRONG + // CORRECT +``` + +### 6. No Direct Association Access in Query Loops + +```cfm +// WRONG - query rows are not objects + + #posts.comments().recordCount# // FAILS + + +// CORRECT - use include to preload, or reload object +tweets = model("Tweet").findAll(include="user", order="createdAt DESC"); + + #tweets.username# // Works - joined data + +``` + +### 7. Non-Existent Form Helpers + +Wheels does NOT have emailField(), passwordField(), numberField(), urlField(). Use textField() with type: + +```cfm +#textField(objectName="user", property="email", type="email")# +#textField(objectName="user", property="password", type="password")# +#textField(objectName="user", property="age", type="number")# +``` + +### 8. Database-Specific SQL in Migrations + +Never use NOW(), DATE_SUB(), CURDATE(), INTERVAL in execute() statements: + +```cfm +// WRONG +execute("INSERT INTO posts (publishedAt) VALUES (NOW())"); + +// CORRECT +var now = Now(); +var formatted = "TIMESTAMP '#DateFormat(now, 'yyyy-mm-dd')# #TimeFormat(now, 'HH:mm:ss')#'"; +execute("INSERT INTO posts (publishedAt) VALUES (#formatted#)"); +``` + +### 9. Use startFormTag() for CSRF Protection + +```cfm + // WRONG - no CSRF token +#startFormTag(action="create")# // CORRECT - auto CSRF +``` + +### 10. linkTo() Escapes HTML in Text Param + +```cfm +// WRONG - HTML will be escaped +#linkTo(text="Brand", controller="home", action="index")# + +// CORRECT - use urlFor() with manual anchor +Brand +``` + +### 11. Update Forms Need Route + Key + +```cfm +#startFormTag(action="update", method="patch")# // WRONG +#startFormTag(route="user", key=user.id, method="patch")# // CORRECT +``` + +### 12. CLI Generator Produces String Booleans + +After using `wheels g migration`, fix createTable calls: + +```cfm +t = createTable(name='users', force='false', id='true'); // WRONG - generated +t = createTable(name='users'); // CORRECT - use defaults +``` + +### 13. timestamps() Includes deletedAt + +```cfm +t.datetime(columnNames="deletedAt"); +t.timestamps(); // WRONG - duplicate deletedAt + +t.timestamps(); // CORRECT - creates createdAt, updatedAt, AND deletedAt +``` + +--- + +## Model Generation + +### Base Template + +Every model MUST follow this structure: + +```cfm +component extends="Model" { + function config() { + table("tablename"); + setPrimaryKey("id"); + + // Associations - ALL named params + hasMany(name="comments", dependent="delete"); + belongsTo(name="author", modelName="User", foreignKey="userId"); + hasOne(name="profile"); + + // Validations - use "properties" (plural) + validatesPresenceOf(properties="title,email"); + validatesUniquenessOf(properties="email", message="already taken"); + validatesFormatOf(properties="email", regEx="^[\w\.-]+@[\w\.-]+\.\w+$"); + validatesLengthOf(properties="username", minimum=3, maximum=50); + + // Callbacks + beforeCreate("setDefaults"); + beforeSave("hashPassword"); + afterCreate("sendWelcomeEmail"); + } +} +``` + +### Association Patterns + +**One-to-Many:** +```cfm +// Parent +hasMany(name="comments", dependent="delete"); +// Child +belongsTo(name="post"); +``` + +**Many-to-Many (through join table):** +```cfm +// Post model +hasMany(name="postTags"); +hasManyThrough(name="tags", through="postTags"); +// Tag model +hasMany(name="postTags"); +hasManyThrough(name="posts", through="postTags"); +// PostTag join model +belongsTo(name="post"); +belongsTo(name="tag"); +``` + +**Self-Referential:** +```cfm +hasMany(name="followings", modelName="Follow", foreignKey="followerId"); +hasMany(name="followers", modelName="Follow", foreignKey="followingId"); +``` + +### Validation Reference + +All use `properties=` (plural). Key options: `message=`, `allowBlank=`, `condition=`. + +```cfm +validatesPresenceOf(properties="name,email"); +validatesUniquenessOf(properties="email"); +validatesUniquenessOf(properties="slug", scope="categoryId"); +validatesFormatOf(properties="email", regEx="^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$"); +validatesLengthOf(properties="password", minimum=8); +validatesNumericalityOf(properties="price", greaterThan=0); +validatesInclusionOf(properties="status", list="draft,published,archived"); +validatesExclusionOf(properties="username", list="admin,root,system"); +validatesConfirmationOf(properties="password"); +validatesPresenceOf(properties="password", condition="isNew()"); +``` + +**Custom:** `validate(methods="myCheck")` then `addError(property="field", message="msg")` in private method. Use `structKeyExists()` before comparing properties. + +### Callbacks + +Available: `beforeValidation`, `beforeValidationOnCreate`, `beforeValidationOnUpdate`, `beforeSave`, `beforeCreate`, `beforeUpdate`, `beforeDelete`, `afterValidation`, `afterSave`, `afterCreate`, `afterUpdate`, `afterDelete`, `afterNew`, `afterFind`. All callback methods must be `private`. + +```cfm +// Slug generation (beforeValidationOnCreate) +private function generateSlug() { + if (!len(this.slug) && len(this.title)) { + this.slug = lCase(reReplace(this.title, "[^a-zA-Z0-9]", "-", "ALL")); + } +} + +// Password hashing (beforeSave) +private function hashPassword() { + if (structKeyExists(this, "password") && len(this.password)) { + this.password = hash(this.password, "SHA-512"); + } +} + +// Default values (beforeCreate) - ALWAYS check structKeyExists +private function setDefaults() { + if (!structKeyExists(this, "viewCount") || !len(this.viewCount)) { + this.viewCount = 0; + } +} +``` + +--- + +## Controller Generation + +### CRUD Template + +```cfm +component extends="Controller" { + + function config() { + verifies(only="show,edit,update,delete", params="key", paramsTypes="integer"); + filters(through="findResource", only="show,edit,update,delete"); + filters(through="requireAuth", except="index,show"); + } + + function index() { + resources = model("Resource").findAll(order="createdAt DESC", page=params.page); + } + + function show() { + // resource loaded by filter + } + + function new() { + resource = model("Resource").new(); + } + + function create() { + resource = model("Resource").new(params.resource); + if (resource.save()) { + flashInsert(success="Resource created!"); + redirectTo(action="show", key=resource.key()); + } else { + flashInsert(error="Please correct the errors below."); + renderView(action="new"); + } + } + + function edit() { + // resource loaded by filter + } + + function update() { + if (resource.update(params.resource)) { + flashInsert(success="Resource updated!"); + redirectTo(action="show", key=resource.key()); + } else { + flashInsert(error="Please correct the errors below."); + renderView(action="edit"); + } + } + + function delete() { + if (resource.delete()) { + flashInsert(success="Resource deleted!"); + } else { + flashInsert(error="Unable to delete."); + } + redirectTo(action="index"); + } + + // PRIVATE filters + + private function findResource() { + resource = model("Resource").findByKey(key=params.key); + if (!isObject(resource)) { + flashInsert(error="Resource not found."); + redirectTo(action="index"); + } + } + + private function requireAuth() { + if (!structKeyExists(session, "userId")) { + flashInsert(error="Please log in."); + redirectTo(controller="sessions", action="new"); + } + } +} +``` + +### Filter Patterns + +Filters MUST be private functions. Always include filters for ALL actions that use loaded data: + +```cfm +// WRONG - show expects resource but filter doesn't cover it +filters(through="findResource", only="edit,update,delete"); + +// CORRECT +filters(through="findResource", only="show,edit,update,delete"); +``` + +**Ownership filter:** +```cfm +private function requireOwnership() { + if (!isObject(resource) || resource.userId != session.userId) { + flashInsert(error="Permission denied."); + redirectTo(action="index"); + } +} +``` + +**Key parameter fallback (profile controllers):** +```cfm +private function findUser() { + if (!structKeyExists(params, "key") && structKeyExists(session, "userId")) { + params.key = session.userId; + } + user = model("User").findByKey(key=params.key); + if (!isObject(user)) { + flashInsert(error="User not found."); + redirectTo(controller="home", action="index"); + } +} +``` + +### Optional Password Update + +For profile edits where password is optional: strip blank password before update. + +```cfm +if (structKeyExists(params.user, "password") && !len(trim(params.user.password))) { + structDelete(params.user, "password"); + structDelete(params.user, "passwordConfirmation"); +} +``` + +### Rendering & Redirects + +```cfm +renderView(action="new"); // Explicit view +provides("json"); renderWith(data=items, format="json"); // JSON API +redirectTo(action="show", key=resource.key()); // After save +redirectTo(controller="home", action="index"); // Cross-controller +``` + +--- + +## View Generation + +### Index View (List) + +```cfm + + +#contentFor(pageTitle="Resources")# + +

Resources

+#linkTo(text="New Resource", action="new", class="btn btn-primary")# + + + +
+

#linkTo(text=resources.title, action="show", key=resources.id)#

+

#left(resources.description, 200)#...

+ #dateFormat(resources.createdAt, "mmm dd, yyyy")# +
+
+ #paginationLinks(prependToLink="page=")# + +

No resources found.

+
+
+``` + +### Show View (Detail) + +Use ``, display properties directly, use `#dateFormat()#` for dates, `#linkTo()#` for edit/back, `#buttonTo(method="delete", confirm="Are you sure?")#` for delete. + +### Form View (New/Edit) + +Key patterns for form views: + +```cfm + + + + + + #startFormTag(action="create", method="post")# + + #startFormTag(route="resource", key=resource.id, method="patch")# + + + +
+ + #textField(objectName="resource", property="title", label=false, class="form-control")# + + + #isArray(titleErr) ? titleErr[1] : titleErr# + +
+ + + #textField(objectName="resource", property="email", type="email", label=false)# + + + #select(objectName="resource", property="categoryId", + options=model("Category").findAll(), valueField="id", textField="name", + includeBlank="-- Select --", label=false)# + + + + + #submitTag(value=resource.isNew() ? "Create" : "Update", class="btn btn-primary")# + #linkTo(text="Cancel", action="index", class="btn")# +#endFormTag()# +
+``` + +### Form Helper Reference + +```cfm +// Text inputs (use type= for email/password/number/url) +#textField(objectName="obj", property="name", class="form-control")# +#textField(objectName="obj", property="email", type="email")# +#textField(objectName="obj", property="password", type="password")# + +// Textarea +#textArea(objectName="obj", property="content", rows=10)# + +// Select +#select(objectName="obj", property="status", options="draft,published,archived")# +#select(objectName="obj", property="categoryId", options=categories, + valueField="id", textField="name", includeBlank="-- Select --")# + +// Checkbox / Radio +#checkBox(objectName="obj", property="active")# +#radioButton(objectName="obj", property="role", tagValue="admin")# + +// Date/Time +#dateSelect(objectName="obj", property="eventDate")# +#timeSelect(objectName="obj", property="eventTime")# +``` + +### allErrors() Return Type + +allErrors() can return string OR array depending on context. Always handle both: + +```cfm + + + #isArray(emailErr) ? emailErr[1] : emailErr# + +``` + +### Layout Essentials + +Layouts must include: `#csrfMetaTags()#` in head, flash message display, `#includeContent()#` for body, `#styleSheetLinkTag()#` and `#javaScriptIncludeTag()#` for assets. Use `#contentFor("pageTitle")#` for dynamic titles. Display flashes with `#flash("success")#`. + +--- + +## Migration Generation + +### Location + +Migrations go in: `app/migrator/migrations/` (NOT `db/migrate/`) + +### Create Table Template + +All migrations: extend `wheels.migrator.Migration`, wrap in `transaction` with try/catch, rollback on error, commit on success. + +```cfm +component extends="wheels.migrator.Migration" { + function up() { + transaction { + try { + t = createTable(name="posts"); + t.string(columnNames="title", allowNull=false, limit=200); + t.text(columnNames="content", allowNull=false); + t.integer(columnNames="userId", allowNull=false); + t.boolean(columnNames="published", default=false); + t.timestamps(); + t.create(); + + addIndex(table="posts", columnNames="userId"); + } catch (any e) { local.exception = e; } + + if (StructKeyExists(local, "exception")) { + transaction action="rollback"; + Throw(errorCode="1", detail=local.exception.detail, + message=local.exception.message, type="any"); + } else { transaction action="commit"; } + } + } + + function down() { dropTable("posts"); } +} +``` + +### Column Types Reference + +```cfm +t.string(columnNames="name", limit=255, allowNull=false, default=""); +t.text(columnNames="description", allowNull=true); +t.integer(columnNames="count", default=0); +t.biginteger(columnNames="largeNumber"); +t.float(columnNames="rating", default=0.0); +t.decimal(columnNames="price", precision=10, scale=2); +t.boolean(columnNames="active", default=true); +t.date(columnNames="birthDate"); +t.datetime(columnNames="publishedAt", allowNull=true); +t.time(columnNames="startTime"); +t.binary(columnNames="fileData"); +t.timestamps(); // Creates createdAt, updatedAt, AND deletedAt +``` + +### Alter Table + +```cfm +addColumn(table="posts", columnType="string", columnName="metaDescription", limit=300, allowNull=true); +changeColumn(table="posts", columnName="title", columnType="string", limit=255); +renameColumn(table="posts", oldColumnName="summary", newColumnName="excerpt"); +removeColumn(table="posts", columnName="oldField"); +addIndex(table="posts", columnNames="metaDescription"); +``` + +### Foreign Keys + +```cfm +addForeignKey(table="posts", referenceTable="users", column="userId", + referenceColumn="id", onDelete="cascade"); +``` + +For self-referential tables (e.g., follows), use explicit `keyName=` to avoid duplicate constraint names: `keyName="FK_follows_follower"` and `keyName="FK_follows_following"`. + +### Join Table Pattern + +```cfm +t = createTable(name="likes"); +t.integer(columnNames="userId", allowNull=false); +t.integer(columnNames="tweetId", allowNull=false); +t.datetime(columnNames="createdAt", allowNull=false); +t.create(); + +// Composite unique index FIRST (covers single-column queries on first column) +addIndex(table="likes", columnNames="userId,tweetId", unique=true); +addIndex(table="likes", columnNames="tweetId"); +``` + +### Data Seeding (Database-Agnostic) + +Always use CFML date functions, never SQL-specific functions: + +```cfm +var now = Now(); +var weekAgo = DateAdd("d", -7, now); +var fmt = "TIMESTAMP '#DateFormat(weekAgo, 'yyyy-mm-dd')# #TimeFormat(weekAgo, 'HH:mm:ss')#'"; + +execute("INSERT INTO posts (title, slug, createdAt, updatedAt) + VALUES ('First Post', 'first-post', #fmt#, #fmt#)"); +``` + +### Development Workflow + +When iterating on migrations during development: +```bash +wheels dbmigrate reset # Clean slate (drops all tables) +wheels dbmigrate latest # Run all migrations fresh +``` + +Never use `reset` in production. Use `wheels dbmigrate latest` only. + +--- + +## Route Configuration + +### File Location: `/config/routes.cfm` + +### Basic Structure + +```cfm + +mapper() + // Resources first + .resources("posts") + .resources("users") + + // Custom routes + .get(name="login", pattern="login", to="sessions##new") + .post(name="authenticate", pattern="login", to="sessions##create") + .delete(name="logout", pattern="logout", to="sessions##delete") + + // Root + .root(to="home##index") + + // Wildcard LAST + .wildcard() +.end(); + +``` + +### Route Ordering + +1. `.resources()` declarations +2. Custom named routes (.get, .post, .patch, .delete) +3. `.root()` route +4. `.wildcard()` — always last + +### Resources + +`.resources("posts")` creates 7 routes: GET /posts (index), GET /posts/new (new), POST /posts (create), GET /posts/[key] (show), GET /posts/[key]/edit (edit), PATCH /posts/[key] (update), DELETE /posts/[key] (delete). + +```cfm +.resources("posts") // All 7 CRUD routes +.resources(name="comments", only="index,show") // Limited actions +.resources(name="photos", except="delete") // Exclude actions +``` + +### Constraints, Namespaces, API Scoping + +```cfm +// Constraints +.get(name="post", pattern="posts/[key]", to="posts##show", constraints={key="[0-9]+"}) + +// Admin namespace: /admin/users -> admin.Users.index() +.namespace("admin") + .resources("users") +.endNamespace() + +// API scoping: /api/v1/posts -> api.v1.Posts.index() +.scope(path="api/v1", module="api.v1") + .resources("posts") +.endScope() +``` + +### Route Helpers + +```cfm +redirectTo(route="post", key=1); // Controller +#linkTo(route="post", key=post.id, text=post.title)# // View link +#urlFor(route="post", key=1)# // URL string +#startFormTag(route="post", key=post.id, method="patch")# // Form +``` + +### No Rails-Style Nested Resources + +Wheels does not support closure-based nesting. Declare resources separately: + +```cfm +.resources("posts") +.resources("comments") +``` + +--- + +## Pre-Generation Checklist + +Before generating any Wheels component, verify: + +- [ ] Does a migration exist for the database table? +- [ ] Is the migration in `app/migrator/migrations/` (not `db/migrate/`)? +- [ ] Does the model have `setPrimaryKey("id")` in config()? +- [ ] Are ALL association/validation calls using consistent named params? +- [ ] Do validations use `properties=` (plural)? +- [ ] Do callbacks use `structKeyExists()` before property access? +- [ ] Do views use `` (not array loops)? +- [ ] Do forms use `startFormTag()` (not raw ``)? +- [ ] Do update forms use `route=` with `key=` (not `action="update"`)? +- [ ] Are date values in migrations using CFML functions (not SQL-specific)? + +## Post-Generation Checklist + +After generating code, validate: + +- [ ] No mixed positional/named arguments in any function call +- [ ] No `emailField()`, `passwordField()`, `numberField()` in views +- [ ] No `ArrayLen()` on query results +- [ ] No database-specific SQL functions in migrations +- [ ] All filter methods are `private` +- [ ] Controller filters cover ALL actions that use loaded data +- [ ] Flash messages provide user feedback on create/update/delete +- [ ] Redirects follow POST/PUT/DELETE actions (PRG pattern) +- [ ] Model can be instantiated: `model("Name").new()` without error diff --git a/.claude/skills/wheels-model-generator/templates/basic-model.cfc b/.claude/skills/wheels-codegen/templates/basic-model.cfc similarity index 100% rename from .claude/skills/wheels-model-generator/templates/basic-model.cfc rename to .claude/skills/wheels-codegen/templates/basic-model.cfc diff --git a/.claude/skills/wheels-model-generator/templates/user-authentication-model.cfc b/.claude/skills/wheels-codegen/templates/user-authentication-model.cfc similarity index 100% rename from .claude/skills/wheels-model-generator/templates/user-authentication-model.cfc rename to .claude/skills/wheels-codegen/templates/user-authentication-model.cfc diff --git a/.claude/skills/wheels-controller-generator/SKILL.md b/.claude/skills/wheels-controller-generator/SKILL.md deleted file mode 100755 index 1c0c137a55..0000000000 --- a/.claude/skills/wheels-controller-generator/SKILL.md +++ /dev/null @@ -1,674 +0,0 @@ ---- -name: Wheels Controller Generator -description: Generate Wheels MVC controllers with CRUD actions, filters, parameter verification, and proper rendering. Use when creating or modifying controllers, adding actions, implementing filters for authentication/authorization, handling form submissions, or rendering views/JSON. Ensures proper Wheels conventions and prevents common controller errors. ---- - -# Wheels Controller Generator - -## When to Use This Skill - -Activate automatically when: -- User requests to create a new controller (e.g., "create a Users controller") -- User wants to add CRUD actions (index, show, new, create, edit, update, delete) -- User needs filters (beforeAction/afterAction) -- User wants authentication or authorization -- User is implementing API endpoints -- User mentions: controller, action, filter, CRUD, API, JSON, render, redirect - -## Critical Patterns - -### ✅ CORRECT Controller Structure - -```cfm -component extends="Controller" { - - function config() { - // Parameter verification - verifies(only="show,edit,update,delete", params="key", paramsTypes="integer"); - - // Filters - filters(through="findResource", only="show,edit,update,delete"); - filters(through="requireAuth", except="index,show"); - } - - // Public action methods - function index() { - resources = model("Resource").findAll(order="createdAt DESC"); - } - - // Private filter methods - private function findResource() { - resource = model("Resource").findByKey(key=params.key); - if (!isObject(resource)) { - flashInsert(error="Resource not found"); - redirectTo(action="index"); - } - } -} -``` - -### ❌ ANTI-PATTERNS to Avoid - -**Don't mix argument styles:** -```cfm -// ❌ WRONG -resource = model("Resource").findByKey(params.key, include="comments"); - -// ✅ CORRECT -resource = model("Resource").findByKey(key=params.key, include="comments"); -``` - -**Don't forget parameter verification:** -```cfm -// ❌ WRONG - No verification, vulnerable to injection -function show() { - post = model("Post").findByKey(key=params.key); -} - -// ✅ CORRECT - Verify params before use -function config() { - verifies(only="show", params="key", paramsTypes="integer"); -} -``` - -**Don't forget CSRF protection in forms:** -```cfm -// ❌ WRONG - Forms without CSRF -#startFormTag(action="create")# - -// ✅ CORRECT - CSRF token included by default -#startFormTag(action="create")# // Wheels adds CSRF automatically -``` - -**CRITICAL: Filter must run for ALL actions that use loaded data:** -```cfm -// ❌ WRONG - Filter doesn't run for show, but show expects 'user' to be loaded -function config() { - filters(through="findUser", only="edit,update,delete"); -} -function show() { - // ERROR: user variable not defined! - renderView(); -} - -// ✅ CORRECT - Filter runs for ALL actions that need the data -function config() { - filters(through="findUser", only="show,edit,update,delete"); -} -function show() { - // user variable loaded by filter - renderView(); -} -``` - -**CRITICAL: Centralize key parameter resolution in filters:** -```cfm -// ❌ WRONG - Duplicated logic in multiple actions -function show() { - if (!structKeyExists(params, "key") && structKeyExists(session, "userId")) { - params.key = session.userId; - } - user = model("User").findByKey(key=params.key); -} - -// ✅ CORRECT - Centralized in filter -private function findUser() { - // Handle session userId fallback - if (!structKeyExists(params, "key") && structKeyExists(session, "userId")) { - params.key = session.userId; - } - user = model("User").findByKey(key=params.key); - if (!isObject(user)) { - flashInsert(error="User not found"); - redirectTo(controller="home", action="index"); - } -} -``` - -## CRUD Controller Template - -### Complete CRUD Implementation - -```cfm -component extends="Controller" { - - function config() { - // Verify key parameter is integer - verifies(only="show,edit,update,delete", params="key", paramsTypes="integer"); - - // Load resource for actions that need it - filters(through="findResource", only="show,edit,update,delete"); - } - - /** - * List all resources - */ - function index() { - resources = model("Resource").findAll( - order="createdAt DESC", - include="associations", // Prevent N+1 queries - page=params.page - ); - } - - /** - * Show single resource - */ - function show() { - // Resource loaded by filter - // Load associated data - associations = resource.associations(order="createdAt ASC"); - } - - /** - * New resource form - */ - function new() { - resource = model("Resource").new(); - } - - /** - * Create new resource - */ - function create() { - resource = model("Resource").new(params.resource); - - if (resource.save()) { - flashInsert(success="Resource created successfully!"); - redirectTo(action="show", key=resource.key()); - } else { - flashInsert(error="Please correct the errors below."); - renderView(action="new"); - } - } - - /** - * Edit resource form - */ - function edit() { - // Resource loaded by filter - } - - /** - * Update resource - */ - function update() { - // Resource loaded by filter - - if (resource.update(params.resource)) { - flashInsert(success="Resource updated successfully!"); - redirectTo(action="show", key=resource.key()); - } else { - flashInsert(error="Please correct the errors below."); - renderView(action="edit"); - } - } - - /** - * Update with optional password change (Task 4 pattern) - * Use this for user profile updates where password change is optional - */ - function updateWithOptionalPassword() { - // User loaded by filter - - // Handle optional password change - if blank, don't change it - if (structKeyExists(params.user, "password")) { - if (!len(trim(params.user.password))) { - structDelete(params.user, "password"); - structDelete(params.user, "passwordConfirmation"); - } - } - - if (user.update(params.user)) { - flashInsert(success="Profile updated successfully!"); - redirectTo(action="show", key=user.key()); - } else { - flashInsert(error="Please correct the errors below."); - renderView(action="edit"); - } - } - - /** - * Delete resource - */ - function delete() { - // Resource loaded by filter - - if (resource.delete()) { - flashInsert(success="Resource deleted successfully!"); - redirectTo(action="index"); - } else { - flashInsert(error="Unable to delete resource."); - redirectTo(action="show", key=resource.key()); - } - } - - /** - * Private filter to load resource - */ - private function findResource() { - resource = model("Resource").findByKey(key=params.key); - - if (!isObject(resource)) { - flashInsert(error="Resource not found."); - redirectTo(action="index"); - } - } -} -``` - -## Filter Patterns - -### Authentication Filter - -```cfm -component extends="Controller" { - - function config() { - // Require authentication for all actions except index and show - filters(through="requireAuth", except="index,show"); - } - - private function requireAuth() { - if (!structKeyExists(session, "userId")) { - flashInsert(error="Please log in to continue."); - redirectTo(controller="sessions", action="new"); - } - } -} -``` - -### Authorization Filter - -```cfm -component extends="Controller" { - - function config() { - filters(through="requireAuth"); - filters(through="requireOwnership", only="edit,update,delete"); - } - - private function requireAuth() { - if (!structKeyExists(session, "userId")) { - flashInsert(error="Please log in."); - redirectTo(controller="sessions", action="new"); - } - } - - private function requireOwnership() { - resource = model("Resource").findByKey(key=params.key); - - if (!isObject(resource) || resource.userId != session.userId) { - flashInsert(error="You don't have permission to access this resource."); - redirectTo(action="index"); - } - } -} -``` - -### Data Loading Filter - -```cfm -component extends="Controller" { - - function config() { - // Load current user for all actions - filters(through="loadCurrentUser"); - } - - private function loadCurrentUser() { - if (structKeyExists(session, "userId")) { - currentUser = model("User").findByKey(key=session.userId); - } - } -} -``` - -## Rendering Patterns - -### Render View - -```cfm -function index() { - resources = model("Resource").findAll(); - // Automatically renders views/resources/index.cfm -} -``` - -### Render Specific View - -```cfm -function create() { - resource = model("Resource").new(params.resource); - - if (!resource.save()) { - // Render the new action's view - renderView(action="new"); - } -} -``` - -### Render JSON (API) - -```cfm -function index() { - resources = model("Resource").findAll(); - - renderWith( - data=resources, - format="json", - status=200 - ); -} -``` - -### Render Partial - -```cfm -function loadMore() { - resources = model("Resource").findAll(page=params.page); - renderPartial(partial="resource", collection=resources); -} -``` - -### Send File - -```cfm -function download() { - resource = model("Resource").findByKey(key=params.key); - - sendFile( - file=resource.filePath, - name=resource.fileName, - disposition="attachment" - ); -} -``` - -## Redirect Patterns - -### Redirect to Action - -```cfm -function create() { - resource = model("Resource").new(params.resource); - - if (resource.save()) { - redirectTo(action="show", key=resource.key()); - } -} -``` - -### Redirect to Controller/Action - -```cfm -function logout() { - structDelete(session, "userId"); - redirectTo(controller="home", action="index"); -} -``` - -### Redirect to URL - -```cfm -function external() { - redirectTo(url="https://wheels.dev"); -} -``` - -### Redirect Back - -```cfm -function cancel() { - redirectTo(back=true, default="index"); -} -``` - -## Flash Message Patterns - -### Success Messages - -```cfm -flashInsert(success="Operation completed successfully!"); -``` - -### Error Messages - -```cfm -flashInsert(error="An error occurred. Please try again."); -``` - -### Multiple Message Types - -```cfm -flashInsert( - success="Resource created!", - notice="Check your email for confirmation." -); -``` - -### Flash Keep (preserve across redirect chain) - -```cfm -flashKeep("success"); -redirectTo(action="intermediate"); -``` - -## Parameter Handling - -### Parameter Verification - -```cfm -function config() { - // Verify key is integer - verifies(only="show", params="key", paramsTypes="integer"); - - // Verify multiple params - verifies(only="create", params="name,email", paramsTypes="string,string"); - - // Verify with default values - verifies(params="page", default=1, paramsTypes="integer"); -} -``` - -### Safe Parameter Access - -```cfm -function index() { - // Use params with defaults - page = structKeyExists(params, "page") ? params.page : 1; - - // Or let Wheels handle it with verifies() - resources = model("Resource").findAll(page=params.page); -} -``` - -### Nested Parameters (Forms) - -```cfm -function create() { - // Form submits: user[name], user[email], user[password] - // Wheels creates: params.user = {name="", email="", password=""} - - user = model("User").new(params.user); -} -``` - -## API Controller Patterns - -### JSON API Controller - -```cfm -component extends="Controller" { - - function config() { - // Set default rendering to JSON - provides("json"); - - // Verify API authentication - filters(through="requireApiAuth"); - } - - function index() { - resources = model("Resource").findAll(); - - renderWith( - data=resources, - format="json", - status=200 - ); - } - - function show() { - resource = model("Resource").findByKey(key=params.key); - - if (!isObject(resource)) { - renderWith( - data={error="Resource not found"}, - format="json", - status=404 - ); - return; - } - - renderWith( - data=resource, - format="json", - status=200 - ); - } - - function create() { - resource = model("Resource").new(params.resource); - - if (resource.save()) { - renderWith( - data=resource, - format="json", - status=201, - location=urlFor(action="show", key=resource.key()) - ); - } else { - renderWith( - data={errors=resource.allErrors()}, - format="json", - status=422 - ); - } - } - - private function requireApiAuth() { - var authHeader = getHTTPRequestData().headers["Authorization"]; - - if (!structKeyExists(local, "authHeader") || !isValidToken(authHeader)) { - renderWith( - data={error="Unauthorized"}, - format="json", - status=401 - ); - abort; - } - } -} -``` - -## Nested Resource Controllers - -### Nested Resource Pattern - -```cfm -// URL: /posts/5/comments -component extends="Controller" { - - function config() { - verifies(params="postId", paramsTypes="integer"); - filters(through="loadPost"); - } - - function index() { - // Post loaded by filter - comments = post.comments(order="createdAt DESC"); - } - - function create() { - comment = model("Comment").new(params.comment); - comment.postId = params.postId; - - if (comment.save()) { - flashInsert(success="Comment added!"); - redirectTo(controller="posts", action="show", key=params.postId); - } - } - - private function loadPost() { - post = model("Post").findByKey(key=params.postId); - - if (!isObject(post)) { - flashInsert(error="Post not found."); - redirectTo(controller="posts", action="index"); - } - } -} -``` - -## Implementation Checklist - -When generating a controller: - -- [ ] Component extends="Controller" -- [ ] config() function defined -- [ ] Parameter verification configured with verifies() -- [ ] Filters defined as private functions -- [ ] All model calls use named parameters -- [ ] Flash messages for user feedback -- [ ] Proper redirects after POST/PUT/DELETE -- [ ] Error handling for not found resources -- [ ] CRUD actions follow conventions -- [ ] Public action methods, private filter methods - -## Testing Controllers - -```cfm -// Test controller instantiation -controller = controller("Resources"); - -// Test action execution -controller.processAction("index"); - -// Test filter execution -controller.processAction("show", {key=1}); - -// Check variables set by action -expect(controller.resources).toBeQuery(); -``` - -## Related Skills - -- **wheels-anti-pattern-detector**: Validates controller code -- **wheels-view-generator**: Creates views for controller actions -- **wheels-test-generator**: Creates controller specs -- **wheels-model-generator**: Creates models used by controller - -## Quick Reference - -### Common Controller Methods -- `renderView()` - Render specific view -- `renderPartial()` - Render partial -- `renderWith()` - Render with format (JSON/XML) -- `redirectTo()` - Redirect to action/URL -- `flashInsert()` - Add flash message -- `sendFile()` - Send file download -- `provides()` - Set default formats -- `abort()` - Stop execution - -### Parameter Verification Types -- `integer`, `string`, `boolean`, `numeric`, `date`, `time`, `email`, `url` - -### Flash Message Types -- `success`, `error`, `warning`, `info`, `notice` - ---- - -**Generated by:** Wheels Controller Generator Skill v1.0 -**Framework:** CFWheels 3.0+ -**Last Updated:** 2025-10-20 diff --git a/.claude/skills/wheels-debugging/SKILL.md b/.claude/skills/wheels-debugging/SKILL.md deleted file mode 100755 index e6615ef0a4..0000000000 --- a/.claude/skills/wheels-debugging/SKILL.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -name: Wheels Debugging -description: Troubleshoot common Wheels errors and provide debugging guidance. Use when encountering errors, exceptions, or unexpected behavior. Provides error analysis, common solutions, and debugging strategies for Wheels applications. ---- - -# Wheels Debugging - -## Common Errors - -### "No matching function [RENDERPAGE] found" - -**Cause:** Using wrong function name - CFWheels uses `renderView()` not `renderPage()` - -**Solution:** Change to correct function name -```cfm -❌ renderPage(action="new") -✅ renderView(action="new") -``` - -### "Missing argument name" Error - -**Cause:** Mixed positional and named arguments - -**Solution:** Use consistent argument style -```cfm -❌ hasMany("comments", dependent="delete") -✅ hasMany(name="comments", dependent="delete") - -❌ .resources("sessions", only="new,create") -✅ .resources(name="sessions", only="new,create") -``` - -### "key [onCreate,onUpdate] doesn't exist" - -**Cause:** Using comma-separated values in validation `when` parameter - -**Solution:** Remove the `when` parameter - validations run on both create and update by default -```cfm -❌ validatesConfirmationOf(properties="password", when="onCreate,onUpdate") -✅ validatesConfirmationOf(properties="password") -``` - -### "Can't cast Object type [Query] to [Array]" - -**Cause:** Using Array functions on queries - -**Solution:** Use query methods -```cfm -❌ ArrayLen(post.comments()) -✅ post.comments().recordCount -``` - -### "Association not found" Error - -**Cause:** Association not properly defined or typo - -**Solution:** -1. Check model config() for association definition -2. Verify association name matches -3. Check model name spelling - -### "Table not found" Error - -**Cause:** Migration not run or table name mismatch - -**Solution:** -```bash -wheels dbmigrate latest -``` - -### "Column not found" Error - -**Cause:** Property doesn't exist in database - -**Solution:** -1. Add column via migration -2. Check property name spelling -3. Verify case sensitivity - -## Debugging Tools - -### Enable Debug Output -```cfm -// In config/settings.cfm -set(showDebugInformation=true); -set(showErrorInformation=true); -``` - -### Inspect Variables -```cfm - - -``` - -### Check SQL Queries -```cfm -// Wheels logs all SQL to debugging output -// Look for red SQL queries (errors) -``` - -## Browser Testing for Debugging - -When debugging UI/view issues, use browser MCP tools to verify: - -### Check Page Rendering -```javascript -// Use available MCP tool (in order of preference): - -// Option 1: Puppeteer MCP (preferred) -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/controller/action") -mcp__puppeteer__puppeteer_screenshot(name="debug-screenshot") - -// Option 2: Playwright MCP -mcp__playwright__playwright_navigate(url="http://localhost:PORT/controller/action") -mcp__playwright__playwright_screenshot() - -// Option 3: Browser MCP (fallback) -mcp__browsermcp__browser_navigate(url="http://localhost:PORT/controller/action") -mcp__browsermcp__browser_screenshot() -``` - -### Verify Form Submissions -```javascript -// Navigate to form -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts/new") - -// Fill and submit -mcp__puppeteer__puppeteer_click(selector="input[name='post[title]']") -// Check for errors in response -``` - -### Check for JavaScript Errors -Use browser console output to identify client-side issues that might affect form submissions or dynamic content. - ---- - -**Generated by:** Wheels Debugging Skill v1.1 diff --git a/.claude/skills/wheels-deployment/SKILL.md b/.claude/skills/wheels-deployment/SKILL.md index 1fac03eec1..13b97a947d 100755 --- a/.claude/skills/wheels-deployment/SKILL.md +++ b/.claude/skills/wheels-deployment/SKILL.md @@ -55,6 +55,3 @@ description: Configure Wheels applications for production deployment with securi - [ ] Use CDN for assets - [ ] Database connection pooling ---- - -**Generated by:** Wheels Deployment Skill v1.0 diff --git a/.claude/skills/wheels-documentation-generator/SKILL.md b/.claude/skills/wheels-documentation-generator/SKILL.md deleted file mode 100755 index cd27d5b57e..0000000000 --- a/.claude/skills/wheels-documentation-generator/SKILL.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: Wheels Documentation Generator -description: Generate documentation comments, README files, and API documentation for Wheels applications. Use when documenting code, creating project READMEs, or generating API docs. ---- - -# Wheels Documentation Generator - -## Function Documentation - -```cfm -/** - * Authenticate user with email and password - * - * @param email User's email address - * @param password User's password (plain text) - * @return User object if authenticated, false otherwise - */ -public any function authenticate(required string email, required string password) { - // Implementation -} -``` - -## Model Documentation - -```cfm -/** - * Post Model - * - * Represents a blog post with associated comments and tags. - * - * Associations: - * - hasMany: comments (dependent delete) - * - hasManyThrough: tags (through postTags) - * - belongsTo: user - * - * Validations: - * - title: presence, length (3-200) - * - slug: presence, uniqueness - * - content: presence, minimum length (10) - */ -component extends="Model" { - // Implementation -} -``` - -## README Template - -```markdown -# Project Name - -## Description - -Brief description of the application. - -## Requirements - -- Wheels 3.0+ -- Lucee 5.x / Adobe ColdFusion 2018+ -- Database (MySQL, PostgreSQL, SQL Server, Oracle, SQLite) - -## Installation - -1. Clone repository -2. Run migrations: `wheels dbmigrate latest` -3. Start server: `wheels server start` - -## Configuration - -Configure database in `config/database.cfm` - -## Testing - -Run tests: `wheels test run` - -## License - -MIT -``` - ---- - -**Generated by:** Wheels Documentation Generator Skill v1.0 diff --git a/.claude/skills/wheels-email-generator/SKILL.md b/.claude/skills/wheels-email-generator/SKILL.md deleted file mode 100755 index 1e052e1526..0000000000 --- a/.claude/skills/wheels-email-generator/SKILL.md +++ /dev/null @@ -1,616 +0,0 @@ ---- -name: Wheels Email Generator -description: Generate email functionality including mailer controllers, email templates, and configuration. Use when sending emails, creating notifications, or implementing transactional emails. Ensures proper email structure, layouts, and testing. ---- - -# Wheels Email Generator - -## When to Use This Skill - -Activate automatically when: -- User wants to send emails -- User mentions: email, mailer, sendMail, notification -- User needs password reset emails -- User wants welcome emails or transactional emails -- User asks about email templates or configuration - -## Email Directory Structure - -``` -/app/ -├── controllers/ -│ └── Mailer.cfc # Email controller -├── views/ -│ └── mailer/ # Email templates -│ ├── layouts/ -│ │ ├── email.cfm # HTML layout -│ │ └── email.txt.cfm # Plain text layout -│ ├── welcome.cfm # HTML version -│ ├── welcome.txt.cfm # Text version -│ ├── resetPassword.cfm -│ └── resetPassword.txt.cfm -└── /config/ - └── settings.cfm # Email configuration -``` - -## Mailer Controller Template - -```cfm -component extends="Controller" { - - function config() { - // Configure email defaults - set( - functionName = "sendEmail", - from = "noreply@yourapp.com", - layout = "email", - detectMultipart = true - ); - } - - /** - * Send welcome email to new user - */ - function welcome(required user) { - sendEmail( - to = arguments.user.email, - subject = "Welcome to Our App!", - template = "mailer/welcome", - user = arguments.user - ); - } - - /** - * Send password reset email - */ - function resetPassword(required user, required token) { - local.resetUrl = URLFor( - route = "passwordReset", - token = arguments.token, - onlyPath = false - ); - - sendEmail( - to = arguments.user.email, - subject = "Reset Your Password", - template = "mailer/resetPassword", - user = arguments.user, - resetUrl = local.resetUrl - ); - } - - /** - * Send order confirmation email - */ - function orderConfirmation(required order) { - sendEmail( - to = arguments.order.customerEmail, - subject = "Order Confirmation - ##" & arguments.order.id, - template = "mailer/orderConfirmation", - order = arguments.order - ); - } - - /** - * Send notification to admin - */ - function adminNotification(required subject, required message) { - sendEmail( - to = application.adminEmail, - subject = arguments.subject, - template = "mailer/adminNotification", - message = arguments.message - ); - } - -} -``` - -## Email Layout (HTML) - views/mailer/layouts/email.cfm - -```cfm - - - - - - - - -
-

Your App Name

-
- -
- ##contentForLayout()## -
- - - - -``` - -## Email Layout (Plain Text) - views/mailer/layouts/email.txt.cfm - -```cfm - -======================================== -YOUR APP NAME -======================================== - -##contentForLayout()## - -======================================== -© #Year(Now())# Your Company -All rights reserved. - -Unsubscribe: [URL] -Privacy: [URL] -======================================== - -``` - -## Email Template Examples - -### Welcome Email (HTML) - views/mailer/welcome.cfm - -```cfm - - - -

Welcome, ##user.firstName##!

- -

Thank you for joining our community. We're excited to have you on board.

- -

Here's what you can do next:

- -
    -
  • Complete your profile
  • -
  • Explore our features
  • -
  • Connect with other users
  • -
- -

- - Get Started - -

- -

If you have any questions, feel free to reply to this email.

- -

Best regards,
-The Team

- -
-``` - -### Welcome Email (Plain Text) - views/mailer/welcome.txt.cfm - -```cfm - - -Welcome, ##user.firstName##! - -Thank you for joining our community. We're excited to have you on board. - -Here's what you can do next: -- Complete your profile -- Explore our features -- Connect with other users - -Get Started: ##URLFor(route='dashboard', onlyPath=false)## - -If you have any questions, feel free to reply to this email. - -Best regards, -The Team - -``` - -### Password Reset Email - views/mailer/resetPassword.cfm - -```cfm - - - - -

Reset Your Password

- -

Hi ##user.firstName##,

- -

We received a request to reset your password. Click the button below to create a new password:

- -

- Reset Password -

- -

Or copy and paste this link into your browser:

-

##resetUrl##

- -

This link will expire in 24 hours.

- -

If you didn't request a password reset, you can safely ignore this email.

- -

Thanks,
-The Security Team

- -
-``` - -## Email Configuration - config/settings.cfm - -```cfm - -// Email Server Configuration -set( - functionName = "sendEmail", - server = "smtp.gmail.com", - port = 587, - username = "your-email@gmail.com", - password = "your-app-password", - useTLS = true, - useSSL = false, - from = "noreply@yourapp.com", - type = "html", - charset = "utf-8" -); - -// Environment-Specific Email Settings -if (application.wheels.environment == "development") { - // Log emails instead of sending - set(functionName = "sendEmail", debug = true, deliver = false); -} - -if (application.wheels.environment == "testing") { - // Send all emails to test account - set(functionName = "sendEmail", to = "test@yourapp.com"); -} - -if (application.wheels.environment == "production") { - // Production settings - set( - functionName = "sendEmail", - server = getEnv("SMTP_SERVER"), - username = getEnv("SMTP_USERNAME"), - password = getEnv("SMTP_PASSWORD"), - deliver = true - ); -} - -``` - -## Using the Mailer - -### In Controllers - -```cfm -component extends="Controller" { - - function create() { - user = model("User").new(params.user); - - if (user.save()) { - // Send welcome email - controller("Mailer").welcome(user); - - redirectTo(route="home", success="Account created! Check your email."); - } else { - renderView(action="new"); - } - } - - function forgotPassword() { - user = model("User").findOne(where="email='#params.email#'"); - - if (isObject(user)) { - // Generate reset token - token = createUUID(); - user.update(resetToken=token, resetTokenExpiry=dateAdd("h", 24, now())); - - // Send reset email - controller("Mailer").resetPassword(user=user, token=token); - - flashInsert(success="Password reset instructions sent to your email."); - } - - redirectTo(action="login"); - } - -} -``` - -### Direct Usage - -```cfm -// Simple email -sendEmail( - to = "user@example.com", - from = "noreply@yourapp.com", - subject = "Test Email", - body = "This is a test email." -); - -// Email with template -sendEmail( - to = "user@example.com", - subject = "Custom Email", - template = "mailer/custom", - customVariable = "value" -); - -// Email with attachments -sendEmail( - to = "user@example.com", - subject = "Invoice", - template = "mailer/invoice", - file = expandPath("./uploads/invoice.pdf"), - fileName = "invoice-##123##.pdf" -); -``` - -## Email with File Attachments - -```cfm -function sendInvoice(required order) { - local.pdfPath = expandPath("./temp/invoice-##arguments.order.id##.pdf"); - - // Generate PDF invoice - generateInvoicePDF(arguments.order, local.pdfPath); - - // Send email with attachment - sendEmail( - to = arguments.order.customerEmail, - subject = "Your Invoice - Order ##arguments.order.id##", - template = "mailer/invoice", - order = arguments.order, - file = local.pdfPath, - fileName = "invoice-##arguments.order.id##.pdf" - ); - - // Clean up temp file - fileDelete(local.pdfPath); -} -``` - -## Email with Multiple Recipients - -```cfm -function sendNewsletter(required subject, required template) { - // Get all subscribed users - subscribers = model("User").findAll(where="subscribed=1"); - - // Send to each subscriber - for (local.subscriber in subscribers) { - sendEmail( - to = local.subscriber.email, - subject = arguments.subject, - template = arguments.template, - user = local.subscriber - ); - } -} -``` - -## Testing Emails - -### Email Test - -```cfm -component extends="wheels.Test" { - - function testWelcomeEmailSent() { - // Create test user - user = model("User").create( - email = "test@example.com", - firstName = "Test" - ); - - // Call mailer - controller("Mailer").welcome(user); - - // Verify email was queued - assert("application.wheels.emailQueue.len() > 0"); - } - - function testPasswordResetEmail() { - user = model("User").findByKey(1); - token = createUUID(); - - controller("Mailer").resetPassword(user=user, token=token); - - // Check email contains reset link - lastEmail = application.wheels.emailQueue[1]; - assert("findNoCase('reset', lastEmail.body) > 0"); - assert("findNoCase(token, lastEmail.body) > 0"); - } - -} -``` - -### Manual Testing - -```cfm -// Test in browser - add route -.get(name="testEmail", pattern="/test/email", to="tests##testEmail") - -// Test controller -function testEmail() { - user = model("User").findByKey(1); - controller("Mailer").welcome(user); - renderText("Email sent! Check server logs."); -} -``` - -## Email Best Practices - -### ✅ DO: -- Always provide both HTML and plain text versions -- Use responsive email layouts (max-width: 600px) -- Include unsubscribe links -- Test emails across different clients -- Use descriptive subject lines -- Handle email failures gracefully -- Queue emails for bulk sending -- Use environment-specific settings - -### ❌ DON'T: -- Send emails synchronously in production -- Hardcode email addresses -- Use complex CSS (limited support) -- Forget error handling -- Send without user consent -- Include sensitive data in emails -- Use JavaScript in emails - -## Common Email Patterns - -### 1. Transactional Emails -```cfm -// Order confirmation, password resets, account verification -- Time-sensitive -- User-triggered -- High priority -``` - -### 2. Notification Emails -```cfm -// Activity updates, mentions, reminders -- Event-driven -- May be batched -- User preferences apply -``` - -### 3. Marketing Emails -```cfm -// Newsletters, promotions -- Bulk sending -- Unsubscribe required -- Scheduled -``` - -## Email Queue Pattern - -```cfm -// Queue email for background processing -function queueEmail(required struct emailData) { - model("EmailQueue").create( - recipient = arguments.emailData.to, - subject = arguments.emailData.subject, - template = arguments.emailData.template, - data = serializeJSON(arguments.emailData), - status = "pending" - ); -} - -// Process queue (run via scheduled task) -function processEmailQueue() { - pending = model("EmailQueue").findAll( - where = "status='pending'", - order = "createdAt", - maxRows = 50 - ); - - for (local.email in pending) { - try { - local.data = deserializeJSON(local.email.data); - sendEmail(argumentCollection=local.data); - local.email.update(status="sent", sentAt=now()); - } catch (any e) { - local.email.update( - status = "failed", - errorMessage = e.message - ); - } - } -} -``` - -## SMTP Providers - -### Gmail -```cfm -server = "smtp.gmail.com" -port = 587 -useTLS = true -// Note: Use app password, not account password -``` - -### SendGrid -```cfm -server = "smtp.sendgrid.net" -port = 587 -username = "apikey" -password = "YOUR_API_KEY" -useTLS = true -``` - -### Mailgun -```cfm -server = "smtp.mailgun.org" -port = 587 -username = "postmaster@yourdomain.mailgun.org" -password = "YOUR_PASSWORD" -useTLS = true -``` - -### AWS SES -```cfm -server = "email-smtp.us-east-1.amazonaws.com" -port = 587 -username = "YOUR_SMTP_USERNAME" -password = "YOUR_SMTP_PASSWORD" -useTLS = true -``` - -## Related Skills - -- **wheels-controller-generator**: Create mailer controllers -- **wheels-view-generator**: Create email templates -- **wheels-auth-generator**: Password reset emails -- **wheels-test-generator**: Test email functionality - ---- - -**Generated by:** Wheels Email Generator Skill v1.0 diff --git a/.claude/skills/wheels-migration-generator/SKILL.md b/.claude/skills/wheels-migration-generator/SKILL.md deleted file mode 100755 index 23727ea372..0000000000 --- a/.claude/skills/wheels-migration-generator/SKILL.md +++ /dev/null @@ -1,709 +0,0 @@ ---- -name: Wheels Migration Generator -description: Generate database-agnostic Wheels migrations for creating tables, altering schemas, and managing database changes. Use when creating or modifying database schema, adding tables, columns, indexes, or foreign keys. Prevents database-specific SQL and ensures cross-database compatibility. ---- - -# Wheels Migration Generator - -## When to Use This Skill - -Activate automatically when: -- User requests to create a migration (e.g., "create posts table") -- User wants to add/modify/remove columns -- User needs to add indexes or foreign keys -- User is changing database schema -- User mentions: migration, database, table, column, index, schema - -## 🚨 CRITICAL: Migration File Location - -**Migrations MUST be in:** `app/migrator/migrations/` -**NOT:** `db/migrate/` or any other location - -After creating migration files, reload Wheels: `curl -s "http://localhost:PORT/?reload=true&password="` - -## Critical Anti-Patterns to Prevent - -### ❌ ANTI-PATTERN 1: Wrong Migration Directory - -**WRONG:** -```bash -# Creating migration in wrong location -db/migrate/20251022072809_CreateUsers.cfc ❌ Won't be found! -``` - -**CORRECT:** -```bash -# Wheels looks for migrations here -app/migrator/migrations/20251022072809_CreateUsers.cfc ✅ Correct! -``` - -### ❌ ANTI-PATTERN 2: timestamps() Includes deletedAt - -**WRONG:** -```cfm -t.datetime(columnNames="deletedAt", allowNull=true); -t.timestamps(); // ❌ Creates duplicate deletedAt! -``` - -**CORRECT:** -```cfm -t.timestamps(); // ✅ Includes createdAt, updatedAt, AND deletedAt -``` - -**Note:** Wheels `t.timestamps()` automatically adds: -- `createdAt` (datetime, NOT NULL) -- `updatedAt` (datetime, NOT NULL) -- `deletedAt` (datetime, NULL) - for soft delete support - -### ❌ ANTI-PATTERN 3: Database-Specific Date Functions - -**NEVER use database-specific functions like DATE_SUB(), NOW(), CURDATE()!** - -**WRONG:** -```cfm -execute("INSERT INTO posts (publishedAt) VALUES (DATE_SUB(NOW(), INTERVAL 1 DAY))"); ❌ MySQL only! -``` - -**CORRECT:** -```cfm -var pastDate = DateAdd("d", -1, Now()); -execute("INSERT INTO posts (publishedAt) VALUES (TIMESTAMP '#DateFormat(pastDate, "yyyy-mm-dd")# #TimeFormat(pastDate, "HH:mm:ss")#')"); ✅ Cross-database! -``` - -## Migration Structure - -### Basic Migration Template - -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - try { - // Your migration code here - - } catch (any e) { - local.exception = e; - } - - if (StructKeyExists(local, "exception")) { - transaction action="rollback"; - Throw( - errorCode="1", - detail=local.exception.detail, - message=local.exception.message, - type="any" - ); - } else { - transaction action="commit"; - } - } - } - - function down() { - // Rollback code here - } -} -``` - -## Create Table Migration - -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - try { - // Create table - t = createTable(name="posts", force=false); - - // String columns - t.string(columnNames="title", allowNull=false, limit=200); - t.string(columnNames="slug", allowNull=false, limit=200); - - // Text columns - t.text(columnNames="content", allowNull=false); - t.text(columnNames="excerpt", allowNull=true); - - // Integer columns - t.integer(columnNames="viewCount", default=0); - t.integer(columnNames="userId", allowNull=false); - - // Boolean columns - t.boolean(columnNames="published", default=false); - - // DateTime columns - t.datetime(columnNames="publishedAt", allowNull=true); - - // Timestamps (createdAt, updatedAt) - t.timestamps(); - - // Create the table - t.create(); - - // Add indexes - addIndex(table="posts", columnNames="slug", unique=true); - addIndex(table="posts", columnNames="userId"); - addIndex(table="posts", columnNames="published,publishedAt"); - - // Add foreign key - addForeignKey( - table="posts", - referenceTable="users", - column="userId", - referenceColumn="id", - onDelete="cascade" - ); - - } catch (any e) { - local.exception = e; - } - - if (StructKeyExists(local, "exception")) { - transaction action="rollback"; - Throw( - errorCode="1", - detail=local.exception.detail, - message=local.exception.message, - type="any" - ); - } else { - transaction action="commit"; - } - } - } - - function down() { - dropTable("posts"); - } -} -``` - -## Alter Table Migration - -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - try { - // Add column - addColumn( - table="posts", - columnType="string", - columnName="metaDescription", - limit=300, - allowNull=true - ); - - // Change column - changeColumn( - table="posts", - columnName="title", - columnType="string", - limit=255, // Changed from 200 - allowNull=false - ); - - // Rename column - renameColumn( - table="posts", - oldColumnName="summary", - newColumnName="excerpt" - ); - - // Remove column - removeColumn(table="posts", columnName="oldField"); - - // Add index - addIndex(table="posts", columnNames="metaDescription"); - - } catch (any e) { - local.exception = e; - } - - if (StructKeyExists(local, "exception")) { - transaction action="rollback"; - Throw( - errorCode="1", - detail=local.exception.detail, - message=local.exception.message, - type="any" - ); - } else { - transaction action="commit"; - } - } - } - - function down() { - removeColumn(table="posts", columnName="metaDescription"); - // Reverse other changes... - } -} -``` - -## Data Migration (Seed Data) - -### Database-Agnostic Date Formatting - -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - try { - // CORRECT: Use CFML date functions - var now = Now(); - var day1 = DateAdd("d", -7, now); - var day2 = DateAdd("d", -6, now); - var day3 = DateAdd("d", -5, now); - - // Format dates for SQL - var nowFormatted = "TIMESTAMP '#DateFormat(now, "yyyy-mm-dd")# #TimeFormat(now, "HH:mm:ss")#'"; - var day1Formatted = "TIMESTAMP '#DateFormat(day1, "yyyy-mm-dd")# #TimeFormat(day1, "HH:mm:ss")#'"; - var day2Formatted = "TIMESTAMP '#DateFormat(day2, "yyyy-mm-dd")# #TimeFormat(day2, "HH:mm:ss")#'"; - - // Insert data - execute(" - INSERT INTO posts (title, slug, content, published, publishedAt, createdAt, updatedAt) - VALUES ( - 'Getting Started with HTMX', - 'getting-started-with-htmx', - '

HTMX is a modern approach to building web applications...

', - 1, - #day1Formatted#, - #day1Formatted#, - #day1Formatted# - ) - "); - - execute(" - INSERT INTO posts (title, slug, content, published, publishedAt, createdAt, updatedAt) - VALUES ( - 'Tailwind CSS Best Practices', - 'tailwind-css-best-practices', - '

Tailwind provides utility-first CSS...

', - 1, - #day2Formatted#, - #day2Formatted#, - #day2Formatted# - ) - "); - - } catch (any e) { - local.exception = e; - } - - if (StructKeyExists(local, "exception")) { - transaction action="rollback"; - Throw( - errorCode="1", - detail=local.exception.detail, - message=local.exception.message, - type="any" - ); - } else { - transaction action="commit"; - } - } - } - - function down() { - execute("DELETE FROM posts WHERE slug IN ('getting-started-with-htmx', 'tailwind-css-best-practices')"); - } -} -``` - -## Column Types - -### Available Column Types - -```cfm -// String (VARCHAR) -t.string(columnNames="name", limit=255, allowNull=false, default=""); - -// Text (TEXT/CLOB) -t.text(columnNames="description", allowNull=true); - -// Integer -t.integer(columnNames="count", default=0, allowNull=false); - -// Big Integer -t.biginteger(columnNames="largeNumber"); - -// Float -t.float(columnNames="rating", default=0.0); - -// Decimal -t.decimal(columnNames="price", precision=10, scale=2); - -// Boolean -t.boolean(columnNames="active", default=true); - -// Date -t.date(columnNames="birthDate"); - -// DateTime -t.datetime(columnNames="publishedAt"); - -// Time -t.time(columnNames="startTime"); - -// Binary -t.binary(columnNames="fileData"); - -// UUID -t.string(columnNames="uuid", limit=36); - -// Timestamps (adds createdAt and updatedAt) -t.timestamps(); -``` - -## 🚨 Production-Tested Critical Fixes - -### 1. CLI Generator Boolean Parameter Bug (CRITICAL) - -**🔴 CRITICAL DISCOVERY:** The CLI generator `wheels g migration` creates migrations with **string boolean values** instead of actual booleans, causing silent failures. - -**Problem Generated by CLI:** -```cfm -// ❌ CLI generates this - STRING values that don't work! -t = createTable(name='users', force='false', id='true', primaryKey='id'); -``` - -**Symptoms:** -- Migration reports success but table isn't created correctly -- "NoPrimaryKey" errors even though migration succeeded -- Primary key not properly configured in database -- Wheels ORM can't find primary key column - -**✅ SOLUTION: Simplify to Use Defaults** -```cfm -// Remove all explicit boolean parameters - let Wheels use defaults -t = createTable(name='users'); // That's it! -t.string(columnNames='username', allowNull=false, limit='50'); -t.timestamps(); -t.create(); -``` - -**Why This Works:** -- Wheels `createTable()` has correct default behavior -- Explicit string booleans (`'false'`, `'true'`) break the logic -- Omitting parameters lets Wheels handle it correctly -- Default: creates 'id' as primary key automatically - -**MANDATORY Post-CLI-Generation Fix:** -```cfm -// 1. Find this pattern in generated migration: -t = createTable(name='tablename', force='false', id='true', primaryKey='id'); - -// 2. Replace with: -t = createTable(name='tablename'); -``` - -**Rule:** -``` -✅ MANDATORY: After CLI generation, remove force/id/primaryKey parameters from createTable() -❌ NEVER use string boolean values: 'false', 'true' -✅ Use actual booleans IF needed: false, true (but defaults are better) -``` - -### 2. Migration Development Workflow - -**🔴 LESSON LEARNED:** When migrations fail or you need to iterate, always reset before running latest. - -**Standard Development Workflow:** -```bash -# 1. Generate migration -wheels g migration CreateUsersTable - -# 2. Edit migration file (fix CLI-generated issues!) - -# 3. ALWAYS reset before running during development -wheels dbmigrate reset # Drops all tables, clean slate -wheels dbmigrate latest # Run all migrations fresh - -# 4. If migration fails, fix it then: -wheels dbmigrate reset # Reset again -wheels dbmigrate latest # Try again -``` - -**Why Reset is Important:** -- Failed migrations may leave partial tables -- Partial tables prevent subsequent migrations from running -- Reset ensures clean database state -- Catches migration errors early - -**Production Workflow (Different!):** -```bash -# In production, NEVER reset! -wheels dbmigrate latest # Only run new migrations -``` - -### 3. Composite Index Ordering (CRITICAL) - -**❌ WRONG ORDER - Causes Index Conflicts:** -```cfm -addIndex(table="likes", columnNames="userId"); // ❌ Creates duplicate -addIndex(table="likes", columnNames="tweetId"); -addIndex(table="likes", columnNames="userId,tweetId", unique=true); -``` - -**✅ CORRECT ORDER - Composite First:** -```cfm -// Composite index FIRST - it covers queries on the first column too! -addIndex(table="likes", columnNames="userId,tweetId", unique=true); -// Then add index for second column only -addIndex(table="likes", columnNames="tweetId"); -``` - -**Why:** A composite index on `(userId, tweetId)` can be used for queries filtering by `userId` alone, making a separate `userId` index redundant. - -### 2. Foreign Key Naming for Self-Referential Tables - -**Problem:** Multiple foreign keys to the same table generate duplicate constraint names in H2: - -```cfm -// ❌ Both try to create "FK_FOLLOWS_USERS" - conflict! -addForeignKey(table="follows", referenceTable="users", column="followerId") -addForeignKey(table="follows", referenceTable="users", column="followingId") -``` - -**Solution A: Explicit Key Names (Preferred for Production)** -```cfm -addForeignKey( - table="follows", - referenceTable="users", - column="followerId", - referenceColumn="id", - keyName="FK_follows_follower", // Explicit unique name - onDelete="cascade" -); - -addForeignKey( - table="follows", - referenceTable="users", - column="followingId", - referenceColumn="id", - keyName="FK_follows_following", // Different unique name - onDelete="cascade" -); -``` - -**Solution B: Skip Foreign Keys (Acceptable for Development)** -```cfm -// Rely on application-layer validation instead -// Indexes provide query performance, foreign keys are optional -addIndex(table="follows", columnNames="followerId,followingId", unique=true); -addIndex(table="follows", columnNames="followingId"); -// Note: Foreign keys omitted to avoid H2 naming conflicts -// Application validates referential integrity -``` - -### 3. Migration Retry with force=true - -When migrations fail mid-transaction (common during development): - -```cfm -// Use force=true to drop and recreate if table exists -t = createTable(name="likes", force=true); // Drops existing table first -``` - -**When to use:** -- ✅ After failed migration leaves partial tables -- ✅ During development when iterating on schema -- ❌ NOT recommended for production (use proper versioning) - -### 4. Join Table Pattern - -For many-to-many relationships (e.g., likes, follows): - -```cfm -t = createTable(name="likes", force=true); -t.integer(columnNames="userId", allowNull=false); -t.integer(columnNames="tweetId", allowNull=false); -t.datetime(columnNames="createdAt", allowNull=false); // Track when relationship created -t.create(); - -// IMPORTANT: Composite unique index FIRST -addIndex(table="likes", columnNames="userId,tweetId", unique=true); -addIndex(table="likes", columnNames="tweetId"); // For reverse lookups -``` - -## Index Management - -```cfm -// Simple index -addIndex(table="posts", columnNames="title"); - -// Unique index -addIndex(table="posts", columnNames="slug", unique=true); - -// Composite index -addIndex(table="posts", columnNames="published,publishedAt"); - -// Remove index -removeIndex(table="posts", indexName="idx_posts_title"); -``` - -## Foreign Key Management - -```cfm -// Add foreign key -addForeignKey( - table="posts", - referenceTable="users", - column="userId", - referenceColumn="id", - onDelete="cascade", // Options: cascade, setNull, setDefault, restrict - onUpdate="cascade" -); - -// Remove foreign key -removeForeignKey(table="posts", keyName="fk_posts_userId"); -``` - -## Join Table Migration - -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - try { - // Create join table for many-to-many - t = createTable(name="postTags", force=false); - t.integer(columnNames="postId", allowNull=false); - t.integer(columnNames="tagId", allowNull=false); - t.timestamps(); - t.create(); - - // Add indexes - addIndex(table="postTags", columnNames="postId"); - addIndex(table="postTags", columnNames="tagId"); - addIndex(table="postTags", columnNames="postId,tagId", unique=true); - - // Add foreign keys - addForeignKey( - table="postTags", - referenceTable="posts", - column="postId", - referenceColumn="id", - onDelete="cascade" - ); - - addForeignKey( - table="postTags", - referenceTable="tags", - column="tagId", - referenceColumn="id", - onDelete="cascade" - ); - - } catch (any e) { - local.exception = e; - } - - if (StructKeyExists(local, "exception")) { - transaction action="rollback"; - Throw( - errorCode="1", - detail=local.exception.detail, - message=local.exception.message, - type="any" - ); - } else { - transaction action="commit"; - } - } - } - - function down() { - dropTable("postTags"); - } -} -``` - -## Implementation Checklist - -When generating a migration: - -- [ ] Extends wheels.migrator.Migration -- [ ] Wrapped in transaction block -- [ ] Try/catch for error handling -- [ ] Rollback on exception -- [ ] Commit on success -- [ ] Use CFML date functions (NOT SQL date functions) -- [ ] Format dates with DateFormat/TimeFormat -- [ ] Include down() method for rollback -- [ ] Add appropriate indexes -- [ ] Add foreign keys where needed -- [ ] Use database-agnostic column types - -## Common Patterns - -### Adding Soft Delete - -```cfm -addColumn( - table="posts", - columnType="datetime", - columnName="deletedAt", - allowNull=true -); -addIndex(table="posts", columnNames="deletedAt"); -``` - -### Adding Full Text Search - -```cfm -// Add column for search -addColumn( - table="posts", - columnType="text", - columnName="searchContent", - allowNull=true -); - -// Create search index (database-specific, document it) -// For PostgreSQL: CREATE INDEX ... USING GIN -// For MySQL: CREATE FULLTEXT INDEX -``` - -### Adding Versioning - -```cfm -addColumn(table="posts", columnType="integer", columnName="version", default=1); -addColumn(table="posts", columnType="integer", columnName="lockVersion", default=0); -``` - -## Migration Commands - -```bash -# Create new migration -wheels g migration CreatePostsTable - -# Run pending migrations -wheels dbmigrate latest - -# Run single migration -wheels dbmigrate up - -# Rollback last migration -wheels dbmigrate down - -# Show migration status -wheels dbmigrate info -``` - -## Related Skills - -- **wheels-model-generator**: Creates models for tables -- **wheels-anti-pattern-detector**: Validates migration code - ---- - -**Generated by:** Wheels Migration Generator Skill v1.0 -**Framework:** CFWheels 3.0+ -**Last Updated:** 2025-10-20 diff --git a/.claude/skills/wheels-model-generator/SKILL.md b/.claude/skills/wheels-model-generator/SKILL.md deleted file mode 100755 index f62669a3b1..0000000000 --- a/.claude/skills/wheels-model-generator/SKILL.md +++ /dev/null @@ -1,750 +0,0 @@ ---- -name: Wheels Model Generator -description: Generate Wheels ORM models with proper validations, associations, and methods. Use when the user wants to create or modify a Wheels model, add validations, define associations (hasMany, belongsTo, hasManyThrough), or implement custom model methods. Prevents common Wheels-specific errors like mixed argument styles and ensures proper CFML syntax. ---- - -# Wheels Model Generator - -## When to Use This Skill - -Activate this skill automatically when: -- User requests to create a new model (e.g., "create a User model") -- User wants to add associations (e.g., "Post hasMany Comments") -- User needs to add validations (e.g., "validate email format") -- User wants to implement custom model methods -- User is modifying existing model configuration -- User mentions: model, validation, association, hasMany, belongsTo, ORM - -## Critical Anti-Patterns to Prevent - -### ❌ ANTI-PATTERN 1: Mixed Argument Styles - -**NEVER mix positional and named arguments in Wheels functions.** - -**WRONG:** -```cfm -hasMany("comments", dependent="delete") // ❌ Mixed -belongsTo("user", foreignKey="userId") // ❌ Mixed -validatesPresenceOf("title", message="Required") // ❌ Mixed -``` - -**CORRECT:** -```cfm -// Option 1: All named parameters (RECOMMENDED) -hasMany(name="comments", dependent="delete") -belongsTo(name="user", foreignKey="userId") -validatesPresenceOf(property="title", message="Required") - -// Option 2: All positional parameters (only when no additional options) -hasMany("comments") -belongsTo("user") -validatesPresenceOf("title") -``` - -### ❌ ANTI-PATTERN 2: Inconsistent Parameter Styles - -**Use the SAME style throughout the entire config() function.** - -**WRONG:** -```cfm -function config() { - hasMany("comments"); // Positional - belongsTo(name="user"); // Named -} -``` - -**CORRECT:** -```cfm -function config() { - hasMany(name="comments"); // All named - belongsTo(name="user"); // All named -} -``` - -### ❌ ANTI-PATTERN 3: Wrong Parameter Names (CRITICAL) - -**🚨 PRODUCTION FINDING: Wheels validation functions use "properties" (PLURAL), not "property"!** - -**WRONG:** -```cfm -validatesPresenceOf(property="username,email") // ❌ "property" parameter doesn't exist! -validatesUniquenessOf(property="email") // ❌ Wrong parameter name -validatesFormatOf(property="email", regEx="...") // ❌ Won't work -validatesLengthOf(property="username", minimum=3) // ❌ Parameter not recognized -``` - -**CORRECT:** -```cfm -validatesPresenceOf(properties="username,email") // ✅ Use "properties" (plural) -validatesUniquenessOf(properties="email") // ✅ Correct -validatesFormatOf(properties="email", regEx="...") // ✅ Works -validatesLengthOf(properties="username", minimum=3) // ✅ Recognized -``` - -**Similarly for custom validation:** -```cfm -validate(methods="customValidation") // ✅ "methods" (plural) -validate(method="customValidation") // ❌ "method" doesn't exist -``` - -## 🚨 Production-Tested Critical Fixes - -### 1. setPrimaryKey() Requirement (CRITICAL) - -**🔴 CRITICAL DISCOVERY:** Even when migrations correctly create primary keys, models **MUST** explicitly declare them using `setPrimaryKey()` in the `config()` method. - -**Problem Symptom:** -``` -Error: "Wheels.NoPrimaryKey: No primary key exists on the users table" -``` - -**Even when migration succeeded:** -```cfm -// Migration appeared successful -t = createTable(name="users"); // Creates id column as primary key -t.create(); // ✅ Reports success -``` - -**Required Fix in Model:** -```cfm -component extends="Model" { - function config() { - table("users"); - setPrimaryKey("id"); // 🚨 MANDATORY - Always add this line! - - // Rest of configuration... - hasMany(name="tweets", dependent="delete"); - validatesPresenceOf(properties="username,email"); - } -} -``` - -**Why This Happens:** -- CLI generators may not add `setPrimaryKey()` to generated models -- Wheels ORM requires explicit primary key declaration in model -- Missing this causes "NoPrimaryKey" error even with correct database schema -- **ALWAYS add `setPrimaryKey("id")` to EVERY model's config() method** - -**Rule:** -``` -✅ MANDATORY: Add setPrimaryKey("id") to EVERY model config() - no exceptions! -``` - -### 2. Property Access in beforeCreate() Callbacks (CRITICAL) - -**🔴 CRITICAL DISCOVERY:** Accessing properties in `beforeCreate()` callbacks without checking existence causes "no accessible Member" errors. - -**Problem Symptom:** -``` -Error: "Component [app.models.User] has no accessible Member with name [FOLLOWERSCOUNT]" -``` - -**❌ WRONG - Causes Error:** -```cfm -component extends="Model" { - function config() { - beforeCreate("setDefaults"); - } - - function setDefaults() { - // ❌ Error if property doesn't exist yet! - if (!len(this.followersCount)) { - this.followersCount = 0; - } - } -} -``` - -**✅ CORRECT - Always Check Existence First:** -```cfm -component extends="Model" { - function config() { - beforeCreate("setDefaults"); - } - - function setDefaults() { - // ✅ Check existence first! - if (!structKeyExists(this, "followersCount") || !len(this.followersCount)) { - this.followersCount = 0; - } - - if (!structKeyExists(this, "followingCount") || !len(this.followingCount)) { - this.followingCount = 0; - } - - if (!structKeyExists(this, "tweetsCount") || !len(this.tweetsCount)) { - this.tweetsCount = 0; - } - } -} -``` - -**Why This Happens:** -- In `beforeCreate()`, properties may not exist yet in the `this` scope -- Direct access like `this.propertyName` throws error if property doesn't exist -- Must use `structKeyExists(this, "propertyName")` before accessing -- This applies to ANY property access in beforeCreate, beforeValidation callbacks - -**Rule:** -``` -✅ MANDATORY: Use structKeyExists(this, "property") before accessing properties in beforeCreate() -``` - -### 3. Complete Production-Ready Model Template - -**Use this template for ALL model generation to avoid common issues:** - -```cfm -component extends="Model" { - - function config() { - // 🚨 MANDATORY: Always set primary key - table("users"); - setPrimaryKey("id"); // CRITICAL - Never omit this! - - // Associations - ALWAYS use named parameters - hasMany(name="tweets", dependent="delete"); - hasMany(name="likes", dependent="delete"); - hasMany(name="followings", foreignKey="followerId", dependent="delete"); - hasMany(name="followers", foreignKey="followingId", dependent="delete"); - - // Validations - Use "properties" (plural) - validatesPresenceOf(properties="username,email,passwordHash"); - validatesUniquenessOf(properties="username,email", message="[property] already taken"); - validatesFormatOf(properties="email", regEx="^[\w\.-]+@[\w\.-]+\.\w+$", message="Invalid email format"); - validatesLengthOf(properties="username", minimum=3, maximum=50); - validatesLengthOf(properties="bio", maximum=160, allowBlank=true); - - // Callbacks - beforeCreate("setDefaults"); - } - - // 🚨 CRITICAL: Always use structKeyExists() in beforeCreate - function setDefaults() { - if (!structKeyExists(this, "followersCount") || !len(this.followersCount)) { - this.followersCount = 0; - } - if (!structKeyExists(this, "followingCount") || !len(this.followingCount)) { - this.followingCount = 0; - } - if (!structKeyExists(this, "tweetsCount") || !len(this.tweetsCount)) { - this.tweetsCount = 0; - } - } - - // Custom methods - function fullName() { - return "@" & this.username; - } - - function isFollowing(required numeric userId) { - var follow = model("Follow").findOne(where="followerId = #this.id# AND followingId = #arguments.userId#"); - return isObject(follow); - } -} -``` - -### 4. CLI Generator Post-Generation Checklist - -**After using CLI `wheels g model` command, ALWAYS review and fix:** - -- [ ] Add `setPrimaryKey("id")` to config() method -- [ ] Change all validation parameters from `property=` to `properties=` -- [ ] Change custom validation from `method=` to `methods=` -- [ ] Add `structKeyExists()` checks in all beforeCreate/beforeValidation callbacks -- [ ] Ensure all association parameters use named style (name=, dependent=) -- [ ] Verify all callback methods are marked `private` -- [ ] Test model instantiation: `model("ModelName").new()` should not error - -## Model Generation Template - -### Basic Model Structure - -```cfm -component extends="Model" { - - function config() { - // Table configuration (optional - only if table name differs from convention) - // table(name="custom_table_name"); - - // Associations - ALWAYS use named parameters for consistency - hasMany(name="association_name", dependent="delete"); - belongsTo(name="parent_model"); - - // Validations - ALWAYS use named parameters - validatesPresenceOf(property="field1,field2"); - validatesUniquenessOf(property="field_name"); - - // Callbacks (optional) - beforeValidationOnCreate("methodName"); - afterCreate("methodName"); - } - - // Custom public methods - public string function customMethod(required string param) { - // Implementation - return result; - } - - // Private helper methods - private void function helperMethod() { - // Implementation - } -} -``` - -## Association Patterns - -### One-to-Many (Parent → Children) - -**Parent Model (Post):** -```cfm -component extends="Model" { - function config() { - hasMany(name="comments", dependent="delete"); - // dependent="delete" removes associated records when parent is deleted - } -} -``` - -**Child Model (Comment):** -```cfm -component extends="Model" { - function config() { - belongsTo(name="post"); - } -} -``` - -### Many-to-Many (Through Join Table) - -**Post Model:** -```cfm -component extends="Model" { - function config() { - hasMany(name="postTags"); - hasManyThrough(name="tags", through="postTags"); - } -} -``` - -**Tag Model:** -```cfm -component extends="Model" { - function config() { - hasMany(name="postTags"); - hasManyThrough(name="posts", through="postTags"); - } -} -``` - -**PostTag Join Model:** -```cfm -component extends="Model" { - function config() { - belongsTo(name="post"); - belongsTo(name="tag"); - } -} -``` - -### Self-Referential Association - -**User Model (for followers/following):** -```cfm -component extends="Model" { - function config() { - hasMany(name="followings", modelName="Follow", foreignKey="followerId"); - hasMany(name="followers", modelName="Follow", foreignKey="followingId"); - } -} -``` - -## Validation Patterns - -### Presence Validation - -```cfm -// Single property -validatesPresenceOf(property="email"); - -// Multiple properties -validatesPresenceOf(property="name,email,password"); - -// With custom message -validatesPresenceOf(property="email", message="Email is required"); - -// Conditional validation -validatesPresenceOf(property="password", condition="isNew()"); -``` - -### Uniqueness Validation - -```cfm -// Basic uniqueness -validatesUniquenessOf(property="email"); - -// Case-insensitive uniqueness -validatesUniquenessOf(property="username", message="Username already taken"); - -// Scoped uniqueness -validatesUniquenessOf(property="slug", scope="categoryId"); -``` - -### Format Validation (Regular Expressions) - -```cfm -// Email format -validatesFormatOf( - property="email", - regEx="^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$", - message="Please enter a valid email address" -); - -// URL format -validatesFormatOf( - property="website", - regEx="^https?://[^\s/$.?#].[^\s]*$", - message="Please enter a valid URL" -); - -// Phone number format (US) -validatesFormatOf( - property="phone", - regEx="^\d{3}-?\d{3}-?\d{4}$", - message="Phone must be in format XXX-XXX-XXXX" -); -``` - -### Length Validation - -```cfm -// Minimum length -validatesLengthOf(property="password", minimum=8); - -// Maximum length -validatesLengthOf(property="title", maximum=200); - -// Exact length -validatesLengthOf(property="zipCode", is=5); - -// Within range -validatesLengthOf( - property="username", - minimum=3, - maximum=20, - message="Username must be between 3 and 20 characters" -); -``` - -### Numericality Validation - -```cfm -// Must be numeric -validatesNumericalityOf(property="age"); - -// Integer only -validatesNumericalityOf(property="quantity", onlyInteger=true); - -// Greater than -validatesNumericalityOf( - property="price", - greaterThan=0, - message="Price must be positive" -); - -// Less than or equal to -validatesNumericalityOf(property="discount", lessThanOrEqualTo=100); - -// Within range -validatesNumericalityOf( - property="rating", - greaterThanOrEqualTo=1, - lessThanOrEqualTo=5 -); -``` - -### Confirmation Validation - -```cfm -// Password confirmation -validatesConfirmationOf(property="password"); - -// Requires passwordConfirmation property in form -// -// -``` - -### Inclusion/Exclusion Validation - -```cfm -// Must be in list -validatesInclusionOf( - property="status", - list="draft,published,archived", - message="Invalid status" -); - -// Cannot be in list -validatesExclusionOf( - property="username", - list="admin,root,system", - message="Username is reserved" -); -``` - -### Custom Validation - -```cfm -component extends="Model" { - function config() { - // Register custom validation method - validate(method="customValidation"); - } - - private void function customValidation() { - // Add error if validation fails - if (len(this.email) && !isValid("email", this.email)) { - addError(property="email", message="Invalid email format"); - } - - // Complex business logic - if (structKeyExists(this, "startDate") && structKeyExists(this, "endDate")) { - if (this.endDate < this.startDate) { - addError(property="endDate", message="End date must be after start date"); - } - } - } -} -``` - -## Callback Patterns - -### Available Callbacks - -```cfm -// Before callbacks -beforeValidation("methodName") -beforeValidationOnCreate("methodName") -beforeValidationOnUpdate("methodName") - -beforeSave("methodName") -beforeCreate("methodName") -beforeUpdate("methodName") -beforeDelete("methodName") - -// After callbacks -afterValidation("methodName") -afterValidationOnCreate("methodName") -afterValidationOnUpdate("methodName") - -afterSave("methodName") -afterCreate("methodName") -afterUpdate("methodName") -afterDelete("methodName") - -// New callbacks -afterNew("methodName") -afterFind("methodName") -``` - -### Common Callback Use Cases - -```cfm -component extends="Model" { - function config() { - // Auto-generate slug before validation - beforeValidationOnCreate("generateSlug"); - - // Set timestamps manually if needed - beforeCreate("setCreatedTimestamp"); - beforeUpdate("setUpdatedTimestamp"); - - // Hash password before saving - beforeSave("hashPassword"); - - // Send welcome email after user creation - afterCreate("sendWelcomeEmail"); - } - - private void function generateSlug() { - if (!len(this.slug) && len(this.title)) { - this.slug = lCase(reReplace(this.title, "[^a-zA-Z0-9]", "-", "ALL")); - this.slug = reReplace(this.slug, "-+", "-", "ALL"); - this.slug = reReplace(this.slug, "^-|-$", "", "ALL"); - } - } - - private void function hashPassword() { - if (structKeyExists(this, "password") && !isHashed(this.password)) { - this.password = hash(this.password, "SHA-512"); - } - } - - private void function sendWelcomeEmail() { - // Email sending logic - sendMail( - to=this.email, - subject="Welcome!", - body="Thanks for signing up." - ); - } -} -``` - -## Custom Method Patterns - -### Common Custom Methods - -```cfm -component extends="Model" { - - // Full name accessor - public string function fullName() { - return this.firstName & " " & this.lastName; - } - - // Excerpt generator - public string function excerpt(numeric length=200) { - if (!structKeyExists(this, "content")) return ""; - var plain = reReplace(this.content, "<[^>]*>", "", "ALL"); - return len(plain) > arguments.length - ? left(plain, arguments.length) & "..." - : plain; - } - - // Status checker - public boolean function isPublished() { - return structKeyExists(this, "published") && this.published; - } - - // Date formatter - public string function formattedDate(string format="yyyy-mm-dd") { - return dateFormat(this.createdAt, arguments.format); - } - - // URL generator - public string function url() { - return "/posts/" & this.slug; - } - - // Safe deletion check - public boolean function canDelete() { - // Don't allow deletion if has associated records - return this.comments().recordCount == 0; - } -} -``` - -## Implementation Checklist - -When generating a model, ensure: - -- [ ] Component extends="Model" -- [ ] config() function defined -- [ ] All association parameters use NAMED style (name=, dependent=) -- [ ] All validation parameters use NAMED style (property=, message=) -- [ ] Consistent parameter style throughout entire config() -- [ ] Association direction matches database relationships -- [ ] Validations match business requirements -- [ ] Custom methods have return type hints (public/private, string/boolean/numeric) -- [ ] Callback methods are private -- [ ] No mixed argument styles anywhere - -## Testing Generated Models - -After generating a model, validate it works: - -```cfm -// Test instantiation -user = model("User").new(); -// Should not throw error - -// Test associations are defined -posts = user.posts(); -// Should return query object - -// Test validations work -user.email = "invalid"; -result = user.valid(); -// Should return false - -errors = user.allErrors(); -// Should contain email validation error - -// Test custom methods -name = user.fullName(); -// Should return concatenated name -``` - -## Common Model Patterns - -### User Authentication Model - -See `templates/user-authentication-model.cfc` for complete example. - -### Soft Delete Model - -```cfm -component extends="Model" { - function config() { - // Mark as deleted instead of actually deleting - beforeDelete("softDelete"); - } - - private void function softDelete() { - this.deletedAt = now(); - this.save(); - abort(); // Prevent actual deletion - } - - public query function findActive() { - return this.findAll(where="deletedAt IS NULL"); - } -} -``` - -### Timestamped Model - -```cfm -component extends="Model" { - function config() { - // Wheels automatically handles createdAt and updatedAt - // if columns exist in database - // No configuration needed! - } -} -``` - -## Related Skills - -- **wheels-anti-pattern-detector**: Validates generated model code -- **wheels-migration-generator**: Creates database schema for model -- **wheels-test-generator**: Creates TestBox specs for model -- **wheels-controller-generator**: Creates controller for model - -## Quick Reference - -### Association Options -- `name` - Association name (required when using named params) -- `dependent` - What to do with associated records: "delete", "deleteAll", "remove", "removeAll" -- `foreignKey` - Custom foreign key column name -- `joinKey` - Custom join key for hasManyThrough -- `modelName` - Override associated model name -- `through` - Join model for hasManyThrough - -### Validation Options -- `property` - Property name(s) to validate (required) -- `message` - Custom error message -- `when` - When to run validation: "onCreate", "onUpdate" -- `condition` - Method name that returns boolean -- `allowBlank` - Allow empty string (default false) - -### Callback Options -- `method` - Method name to call (or array of method names) - ---- - -**Generated by:** Wheels Model Generator Skill v1.0 -**Framework:** CFWheels 3.0+ -**Last Updated:** 2025-10-20 diff --git a/.claude/skills/wheels-plugin-generator/SKILL.md b/.claude/skills/wheels-plugin-generator/SKILL.md deleted file mode 100755 index a8f920cd56..0000000000 --- a/.claude/skills/wheels-plugin-generator/SKILL.md +++ /dev/null @@ -1,459 +0,0 @@ ---- -name: Wheels Plugin Generator -description: Generate Wheels plugins with proper structure, configuration, and ForgeBox packaging. Use when creating plugins, extending Wheels functionality, or packaging reusable components. Ensures plugins follow Wheels conventions and can be easily shared via ForgeBox. ---- - -# Wheels Plugin Generator - -## When to Use This Skill - -Activate automatically when: -- User wants to create a plugin -- User mentions: plugin, extend wheels, reusable component -- User wants to package functionality for ForgeBox -- User needs to add plugin configuration -- User asks about plugin structure - -## Plugin Directory Structure - -``` -/plugins/YourPlugin/ -├── index.cfm # Plugin entry point -├── box.json # ForgeBox package metadata -├── README.md # Plugin documentation -├── config/ -│ └── settings.cfm # Plugin configuration -├── controllers/ # Plugin controllers (optional) -├── models/ # Plugin models (optional) -├── views/ # Plugin views (optional) -│ └── helpers/ # View helpers -├── events/ # Event handlers (optional) -├── db/ # Database migrations (optional) -│ └── migrate/ -└── tests/ # Plugin tests - └── PluginTest.cfc -``` - -## Plugin Entry Point (index.cfm) - -```cfm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -## Plugin Configuration (config/settings.cfm) - -```cfm - -// Plugin-specific settings -set(functionName="pluginSetting", value="default"); -set(functionName="anotherSetting", per="environment", dev=true, prod=false); - -// Environment-specific configuration -if (application.wheels.environment == "production") { - set(functionName="productionSetting", value="prod-value"); -} - -``` - -## Plugin box.json Template - -```json -{ - "name": "YourPlugin", - "slug": "your-plugin", - "version": "1.0.0", - "author": "Your Name ", - "location": "forgeboxStorage", - "type": "cfwheels-plugins", - "homepage": "https://github.com/yourusername/your-plugin", - "documentation": "https://github.com/yourusername/your-plugin/wiki", - "repository": { - "type": "git", - "URL": "https://github.com/yourusername/your-plugin" - }, - "bugs": "https://github.com/yourusername/your-plugin/issues", - "shortDescription": "Brief description of your plugin", - "description": "Detailed description of what your plugin does", - "keywords": [ - "cfwheels", - "plugin", - "feature" - ], - "private": false, - "engines": [ - { - "type": "lucee", - "version": ">=5.0.0" - }, - { - "type": "adobe", - "version": ">=2018.0.0" - } - ], - "defaultPort": 0, - "projectURL": "", - "license": [ - { - "type": "Apache-2.0", - "URL": "https://www.apache.org/licenses/LICENSE-2.0" - } - ], - "contributors": [], - "dependencies": {}, - "devDependencies": {}, - "installPaths": {}, - "ignore": [ - "**/.*", - "test", - "tests" - ] -} -``` - -## Plugin README.md Template - -```markdown -# Your Plugin Name - -Brief description of what your plugin does. - -## Requirements - -- CFWheels 2.x or higher -- Lucee 5+ or Adobe ColdFusion 2018+ - -## Installation - -### Option 1: CommandBox (Recommended) -```bash -box install your-plugin -``` - -### Option 2: Manual Installation -1. Download the plugin -2. Extract to `/plugins/YourPlugin/` -3. Reload your Wheels application - -## Configuration - -Add to `config/settings.cfm`: - -```cfm - -set(functionName="pluginSetting", value="yourValue"); - -``` - -## Usage - -### Basic Example -```cfm -// In controller -result = myPluginMethod("Hello World"); - -// In view -#myPluginHelper()# -``` - -### Advanced Example -```cfm -// Your advanced usage examples here -``` - -## API Reference - -### Global Methods - -#### myPluginMethod(text) -Description of what this method does. - -**Parameters:** -- `text` (string, required) - Description - -**Returns:** string - -**Example:** -```cfm -result = myPluginMethod("test"); -``` - -## Testing - -Run plugin tests: -```bash -box testbox run -``` - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Submit a pull request - -## License - -Apache License 2.0 - -## Credits - -Created by [Your Name](https://yourwebsite.com) -``` - -## Plugin Event Handlers - -```cfm - - - - - - - - - - - - - - - - - - - - - - - -``` - -## Available Events - -Wheels plugins can hook into these events: - -- `onApplicationStart` - Application initialization -- `onRequestStart` - Beginning of each request -- `onRequestEnd` - End of each request -- `onSessionStart` - New session created -- `onSessionEnd` - Session expires -- `onError` - Error occurs -- `onMissingMethod` - Method not found -- `onMissingTemplate` - Template not found - -## Plugin with Database Migrations - -``` -/plugins/YourPlugin/ -└── db/ - └── migrate/ - ├── 001_CreatePluginTable.cfc - └── 002_AddPluginColumn.cfc -``` - -**Migration Example:** -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - t = createTable(name="plugin_data"); - t.string(columnNames="name"); - t.text(columnNames="data"); - t.timestamps(); - t.create(); - } - } - - function down() { - dropTable("plugin_data"); - } -} -``` - -## Plugin Testing - -```cfm -component extends="wheels.Test" { - - function setup() { - super.setup(); - // Setup test fixtures - } - - function teardown() { - // Cleanup - super.teardown(); - } - - function testPluginMethodExists() { - assert("structKeyExists(variables.wheels, 'myPluginMethod')"); - } - - function testPluginMethodReturnsCorrectly() { - result = myPluginMethod("test"); - assert("result == 'Plugin: test'"); - } - - function testPluginConfiguration() { - setting = get("pluginSetting"); - assert("setting == 'expectedValue'"); - } -} -``` - -## Publishing to ForgeBox - -### 1. Prepare Plugin -```bash -# Ensure box.json is configured -box show - -# Test locally -box install -``` - -### 2. Publish -```bash -# Login to ForgeBox -box forgebox login - -# Publish plugin -box forgebox publish -``` - -### 3. Version Management -```bash -# Update version -box bump --major # 1.0.0 -> 2.0.0 -box bump --minor # 1.0.0 -> 1.1.0 -box bump --patch # 1.0.0 -> 1.0.1 - -# Republish -box forgebox publish -``` - -## Common Plugin Patterns - -### 1. Validation Plugin -```cfm - - - - - - -``` - -### 2. Helper Plugin -```cfm - - - - -``` - -### 3. Model Extension Plugin -```cfm - - - - - - - - - -``` - -## Plugin Best Practices - -### ✅ DO: -- Use unique function names (prefix with plugin name) -- Provide configuration options via settings -- Include comprehensive documentation -- Add tests for all functionality -- Follow Wheels naming conventions -- Version using semantic versioning -- Include LICENSE file - -### ❌ DON'T: -- Overwrite core Wheels methods -- Use generic function names (risk conflicts) -- Hardcode configuration values -- Skip error handling -- Forget to document configuration options - -## Debugging Plugins - -### Enable Plugin Debug -```cfm -// In config/settings.cfm -set(showDebugInformation=true); - -// Check plugin loaded - -``` - -### Check Plugin Methods -```cfm -// List available methods - - -// Test plugin method exists - - Works! - -``` - -## Related Skills - -- **wheels-model-generator**: Create models that plugins extend -- **wheels-controller-generator**: Create controllers that use plugins -- **wheels-migration-generator**: Create plugin migrations -- **wheels-test-generator**: Test plugin functionality - ---- - -**Generated by:** Wheels Plugin Generator Skill v1.0 diff --git a/.claude/skills/wheels-refactoring/SKILL.md b/.claude/skills/wheels-refactoring/SKILL.md index 417e1d2c7b..24ce248818 100755 --- a/.claude/skills/wheels-refactoring/SKILL.md +++ b/.claude/skills/wheels-refactoring/SKILL.md @@ -108,6 +108,3 @@ private function sendWelcomeEmail() { } ``` ---- - -**Generated by:** Wheels Refactoring Skill v1.0 diff --git a/.claude/skills/wheels-routing-generator/SKILL.md b/.claude/skills/wheels-routing-generator/SKILL.md deleted file mode 100755 index a0d651435d..0000000000 --- a/.claude/skills/wheels-routing-generator/SKILL.md +++ /dev/null @@ -1,632 +0,0 @@ ---- -name: Wheels Routing Generator -description: Generate RESTful routes, nested routes, and custom routing patterns for Wheels applications. Use when defining URL structure, creating RESTful resources, or implementing custom route patterns. Ensures proper HTTP verb mapping and route constraints. ---- - -# Wheels Routing Generator - -## When to Use This Skill - -Activate automatically when: -- User wants to create routes -- User mentions: routes, routing, URL structure, RESTful -- User needs nested resources -- User wants custom route patterns -- User asks about URL mapping or route constraints - -## Routes Configuration File - -**Location:** `/config/routes.cfm` - -```cfm - -// Basic structure -mapper() - // Your routes here -.end(); - -``` - -## Basic Route Patterns - -### Simple Routes - -```cfm - -mapper() - // GET request to /about -> Pages.about() - .get(name="about", pattern="about", to="pages##about") - - // POST request to /contact -> Pages.contact() - .post(name="contact", pattern="contact", to="pages##contact") - - // Multiple HTTP verbs - .match(name="search", pattern="search", to="posts##search", methods="GET,POST") - - // Route with parameter - .get(name="post", pattern="posts/[key]", to="posts##show") - - // Root route (home page) - .root(to="pages##index") -.end(); - -``` - -### RESTful Resources - -```cfm - -mapper() - // Complete RESTful resource - // Creates 7 routes: index, new, create, show, edit, update, delete - .resources("posts") - - // Limit to specific actions - .resources(name="comments", only="index,show") - .resources(name="photos", except="delete") -.end(); - -``` - -**Generated Routes for `.resources("posts")`:** - -| HTTP Verb | Path | Action | Purpose | -|-----------|-------------------|---------|----------------------| -| GET | /posts | index | List all posts | -| GET | /posts/new | new | Show create form | -| POST | /posts | create | Create new post | -| GET | /posts/[key] | show | Show single post | -| GET | /posts/[key]/edit | edit | Show edit form | -| PATCH/PUT | /posts/[key] | update | Update post | -| DELETE | /posts/[key] | delete | Delete post | - -## Nested Resources - -### Parent-Child Resources - -```cfm - -mapper() - // Posts with nested comments - .resources("posts") - .resources(name="comments", nested=true) - - // /posts/1/comments - shows comments for post 1 - // /posts/1/comments/2 - shows comment 2 for post 1 -.end(); - -``` - -### Multiple Level Nesting - -```cfm - -mapper() - // Blog -> Post -> Comments - .resources("blogs") - .resources(name="posts", nested=true) - .resources(name="comments", nested=true) - - // /blogs/1/posts/2/comments/3 -.end(); - -``` - -### Shallow Nesting (Recommended) - -```cfm - -mapper() - // Nested for creation only - .resources("posts") - .resources(name="comments", nested=true, only="new,create") - - // Standalone for viewing/editing - .resources("comments", except="new,create") - - // Result: - // POST /posts/1/comments (create with parent context) - // GET /comments/1 (view without deep nesting) - // PATCH /comments/1 (update without deep nesting) -.end(); - -``` - -## Custom Route Patterns - -### Named Routes with Parameters - -```cfm - -mapper() - // Single parameter - .get(name="userProfile", pattern="users/[username]", to="users##show") - - // Multiple parameters - .get( - name="blogPost", - pattern="[year]/[month]/[slug]", - to="posts##show" - ) - - // Optional parameters - .get( - name="search", - pattern="search/[[category]]/[[tag]]", - to="search##index" - ) -.end(); - -``` - -### Wildcard Routes - -```cfm - -mapper() - // Catch-all pattern (use sparingly!) - .get( - name="pages", - pattern="pages/[*path]", - to="pages##show" - ) - // Matches: /pages/about, /pages/help/faq, etc. -.end(); - -``` - -### Route Constraints - -```cfm - -mapper() - // Numeric constraint - .get( - name="post", - pattern="posts/[key]", - to="posts##show", - constraints={key="[0-9]+"} - ) - - // Alpha constraint - .get( - name="tag", - pattern="tags/[slug]", - to="tags##show", - constraints={slug="[a-z-]+"} - ) - - // Date constraint - .get( - name="archive", - pattern="[year]/[month]", - to="posts##archive", - constraints={ - year="[0-9]{4}", - month="[0-9]{2}" - } - ) -.end(); - -``` - -## Namespace and Scoped Routes - -### Controller Namespace - -```cfm - -mapper() - // Admin section - .namespace("admin") - .resources("users") - .resources("posts") - .resources("settings") - .endNamespace() - - // Maps to: - // /admin/users -> admin.Users.index() - // /admin/posts/1 -> admin.Posts.show(key=1) -.end(); - -``` - -### Path Scoping - -```cfm - -mapper() - // Scope multiple routes under a path - .scope(path="api/v1", module="api.v1") - .resources("posts") - .resources("comments") - .endScope() - - // Maps to: - // /api/v1/posts -> api.v1.Posts.index() - // /api/v1/comments -> api.v1.Comments.index() -.end(); - -``` - -## API Versioning Routes - -```cfm - -mapper() - // API v1 - .scope(path="api/v1", module="api.v1") - .resources("posts") - .resources("users") - .endScope() - - // API v2 - .scope(path="api/v2", module="api.v2") - .resources("posts") - .resources("users") - .endScope() - - // Latest (redirects to current version) - .get(name="apiLatest", pattern="api/[*path]", to="api##redirectToLatest") -.end(); - -``` - -## RESTful Member and Collection Routes - -### Member Routes (Single Resource) - -```cfm - -mapper() - .resources("posts") - // Custom actions on single post - .member(name="publish", to="posts##publish", method="patch") - .member(name="archive", to="posts##archive", method="post") - .endResources() - - // Maps to: - // PATCH /posts/1/publish - // POST /posts/1/archive -.end(); - -``` - -### Collection Routes (All Resources) - -```cfm - -mapper() - .resources("posts") - // Custom actions on collection - .collection(name="search", to="posts##search", method="get") - .collection(name="export", to="posts##export", method="get") - .endResources() - - // Maps to: - // GET /posts/search - // GET /posts/export -.end(); - -``` - -## Route Helpers in Controllers - -### Generating URLs - -```cfm -// In controllers/views -urlFor(route="post", key=1) -// -> /posts/1 - -urlFor(route="editPost", key=1) -// -> /posts/1/edit - -urlFor(route="posts") -// -> /posts - -// With query parameters -urlFor(route="posts", params={tag="rails", page=2}) -// -> /posts?tag=rails&page=2 - -// Absolute URLs -urlFor(route="post", key=1, onlyPath=false) -// -> http://yoursite.com/posts/1 -``` - -### Redirecting - -```cfm -// Redirect to named route -redirectTo(route="posts") -redirectTo(route="post", key=1) - -// Redirect with flash -redirectTo(route="posts", success="Post created!") -``` - -### Link Helpers in Views - -```cfm -// Link to route -#linkTo(route="posts", text="All Posts")# - -// Link with parameters -#linkTo(route="post", key=postId, text=post.title)# - -// Link with HTML attributes -#linkTo(route="post", key=1, text="Read More", class="btn btn-primary")# -``` - -## Form Routes - -```cfm - - - #startFormTag(route="posts", method="post")# - - #endFormTag()# - - - #startFormTag(route="post", key=post.id, method="patch")# - - #endFormTag()# - - - #linkTo( - route="post", - key=post.id, - method="delete", - text="Delete", - confirm="Are you sure?" - )# - -``` - -## Advanced Routing Patterns - -### Subdomain Routes - -```cfm - -mapper() - // Admin subdomain - .scope(subdomain="admin") - .resources("users") - .resources("settings") - .endScope() - - // API subdomain - .scope(subdomain="api") - .resources("posts") - .endScope() - - // Wildcard subdomain - .scope(subdomain="[account]") - .root(to="accounts##dashboard") - .resources("projects") - .endScope() -.end(); - -``` - -### Redirect Routes - -```cfm - -mapper() - // Permanent redirect - .redirect(from="old-blog", to="posts", statusCode=301) - - // Redirect to external URL - .redirect( - from="docs", - to="https://docs.yoursite.com", - statusCode=302 - ) -.end(); - -``` - -### Route Concerns (Reusable Route Sets) - -```cfm - -mapper() - // Define concern - .concern(name="commentable") - .resources(name="comments", nested=true) - .endConcern() - - // Use concern - .resources("posts") - .concerns("commentable") - .endResources() - - .resources("photos") - .concerns("commentable") - .endResources() - - // Both posts and photos now have comments routes -.end(); - -``` - -## Testing Routes - -### View All Routes - -```cfm -// Create a test action -function testRoutes() { - routes = application.wheels.routes; - writeDump(routes); - abort; -} -``` - -### Check Route Mapping - -```cfm -// Test if route exists -if (structKeyExists(application.wheels.namedRoutes, "post")) { - // Route exists -} - -// Get route pattern -pattern = application.wheels.namedRoutes.post.pattern; -``` - -## Route Best Practices - -### ✅ DO: -- Use RESTful conventions (resources) -- Keep nesting shallow (max 2 levels) -- Use named routes for flexibility -- Add route constraints for validation -- Group related routes with scopes -- Use proper HTTP verbs -- Follow REST conventions consistently - -### ❌ DON'T: -- Over-nest resources (avoid `/a/b/c/d/e`) -- Use catch-all routes carelessly -- Hardcode URLs in code (use urlFor) -- Mix REST and non-REST patterns -- Create ambiguous routes -- Forget route constraints on IDs -- Use GET for destructive actions - -## Common Route Patterns - -### Blog Application - -```cfm - -mapper() - .root(to="posts##index") - - // Public routes - .get(name="about", pattern="about", to="pages##about") - .get(name="contact", pattern="contact", to="pages##contact") - .post(name="contactSubmit", pattern="contact", to="pages##sendContact") - - // Blog routes - .resources("posts", only="index,show") - .get(name="archive", pattern="archive/[year]/[month]", to="posts##archive") - .get(name="tag", pattern="tags/[slug]", to="posts##byTag") - - // Admin section - .namespace("admin") - .resources("posts") - .resources("comments") - .resources("tags") - .endNamespace() - - // Authentication - .get(name="login", pattern="login", to="sessions##new") - .post(name="sessionCreate", pattern="login", to="sessions##create") - .delete(name="logout", pattern="logout", to="sessions##delete") -.end(); - -``` - -### E-commerce Application - -```cfm - -mapper() - .root(to="products##index") - - // Products - .resources("products", only="index,show") - .get(name="category", pattern="categories/[slug]", to="products##category") - - // Shopping cart - .get(name="cart", pattern="cart", to="cart##show") - .post(name="addToCart", pattern="cart/add", to="cart##add") - .delete(name="removeFromCart", pattern="cart/remove/[key]", to="cart##remove") - - // Checkout - .get(name="checkout", pattern="checkout", to="checkout##index") - .post(name="processCheckout", pattern="checkout", to="checkout##process") - - // User account - .get(name="account", pattern="account", to="users##show") - .resources("orders", only="index,show") - - // Admin - .namespace("admin") - .resources("products") - .resources("orders") - .resources("customers") - .endNamespace() -.end(); - -``` - -### API Routes - -```cfm - -mapper() - // API v1 - .scope(path="api/v1", module="api.v1") - // Resources with token auth - .resources("posts") - .resources("comments") - .resources("users", except="new,edit") - - // Custom endpoints - .post(name="apiLogin", pattern="auth/login", to="auth##login") - .post(name="apiLogout", pattern="auth/logout", to="auth##logout") - .get(name="apiProfile", pattern="me", to="users##profile") - .endScope() -.end(); - -``` - -## Debugging Routes - -### List All Routes - -Add to a controller: - -```cfm -function listRoutes() { - routes = []; - - for (local.key in application.wheels.namedRoutes) { - local.route = application.wheels.namedRoutes[local.key]; - arrayAppend(routes, { - name = local.key, - pattern = local.route.pattern, - controller = local.route.controller, - action = local.route.action, - methods = local.route.methods - }); - } - - writeDump(routes); - abort; -} -``` - -### Test Route Generation - -```cfm -// Test in view/controller - - -``` - -## Related Skills - -- **wheels-controller-generator**: Create controllers for routes -- **wheels-api-generator**: Create API routes -- **wheels-view-generator**: Create views for routes -- **wheels-test-generator**: Test routing - ---- - -**Generated by:** Wheels Routing Generator Skill v1.0 diff --git a/.claude/skills/wheels-scaffold/SKILL.md b/.claude/skills/wheels-scaffold/SKILL.md new file mode 100644 index 0000000000..455e036759 --- /dev/null +++ b/.claude/skills/wheels-scaffold/SKILL.md @@ -0,0 +1,443 @@ +--- +name: Wheels Scaffold +description: Generate authentication systems, RESTful APIs, email/mailer functionality, and plugins. Use when implementing login/logout, API endpoints, transactional emails, or reusable plugin packages. +--- + +# Wheels Scaffold + +Activate when user mentions: auth, login, signup, API, REST, JSON endpoint, email, mailer, notification, plugin, ForgeBox. + +--- + +## 1. Authentication Scaffold + +### User Model with Password Hashing + +```cfm +component extends="Model" { + function config() { + validatesPresenceOf(property="email,password"); + validatesUniquenessOf(property="email"); + validatesFormatOf(property="email", regEx="^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$"); + validatesLengthOf(property="password", minimum=8); + validatesConfirmationOf(property="password"); + beforeSave("hashPassword"); + } + + private function hashPassword() { + if (structKeyExists(this, "password") && len(this.password) && !isHashed(this.password)) + this.password = hash(this.password, "SHA-512"); + } + + private boolean function isHashed(required string password) { + return len(arguments.password) == 128; + } + + public any function authenticate(required string email, required string password) { + var user = this.findOne(where="email = '#arguments.email#'"); + if (!isObject(user)) return false; + return (user.password == hash(arguments.password, "SHA-512")) ? user : false; + } + + public void function generateResetToken() { + this.resetToken = hash(createUUID() & now(), "SHA-256"); + this.resetTokenExpiry = dateAdd("h", 1, now()); + } + + public boolean function isResetTokenValid() { + if (!structKeyExists(this, "resetToken") || !len(this.resetToken)) return false; + if (!structKeyExists(this, "resetTokenExpiry")) return false; + return dateCompare(now(), this.resetTokenExpiry) < 0; + } + + public void function clearResetToken() { + this.resetToken = ""; + this.resetTokenExpiry = ""; + } +} +``` + +### Sessions Controller (Login/Logout) + +```cfm +component extends="Controller" { + function new() { /* Show login form */ } + + function create() { + var user = model("User").authenticate(email=params.email, password=params.password); + if (isObject(user)) { + session.userId = user.id; + flashInsert(success="Welcome back!"); + redirectTo(controller="home", action="index"); + } else { + flashInsert(error="Invalid email or password"); + renderPage(action="new"); + } + } + + function delete() { + structDelete(session, "userId"); + flashInsert(success="You have been logged out"); + redirectTo(controller="home", action="index"); + } +} +``` + +### Authentication Filter (add to any controller) + +```cfm +function config() { filters(through="requireAuth"); } + +private function requireAuth() { + if (!structKeyExists(session, "userId")) { + flashInsert(error="Please log in"); + redirectTo(controller="sessions", action="new"); + } +} +``` + +### Password Reset Controller + +```cfm +component extends="Controller" { + // SECURITY: Always show same message to prevent email enumeration + function create() { + user = model("User").findOne(where="email = '#params.email#' AND deletedAt IS NULL"); + if (isObject(user)) { user.generateResetToken(); user.save(); /* Send email */ } + flashInsert(success="If that email is in our system, we've sent reset instructions."); + redirectTo(controller="sessions", action="new"); + } + + function edit() { + user = model("User").findOne(where="resetToken = '#params.token#' AND deletedAt IS NULL"); + if (!isObject(user) || !user.isResetTokenValid()) { + flashInsert(error="Invalid or expired reset link."); + redirectTo(controller="sessions", action="new"); return; + } + token = params.token; + } + + function update() { + user = model("User").findOne(where="resetToken = '#params.token#' AND deletedAt IS NULL"); + if (!isObject(user) || !user.isResetTokenValid()) { + flashInsert(error="Invalid or expired reset link."); + redirectTo(controller="sessions", action="new"); return; + } + user.password = params.password; + user.passwordConfirmation = params.passwordConfirmation; + user.clearResetToken(); + if (user.save()) { + session.userId = user.id; + redirectTo(controller="home", action="index"); + } + } +} +``` + +**Security:** Same success message on reset (prevents enumeration), 1-hour single-use tokens, SHA-256 hash with UUID, HTTPS only in production. + +--- + +## 2. API Scaffold + +### RESTful API Controller + +```cfm +component extends="Controller" { + function config() { + provides("json"); + verifies(only="show,update,delete", params="key", paramsTypes="integer"); + filters(through="requireApiAuth"); + } + + function index() { + renderWith(data=model("Resource").findAll(order="createdAt DESC"), format="json", status=200); + } + + function show() { + resource = model("Resource").findByKey(key=params.key); + if (!isObject(resource)) { renderWith(data={error="Not found"}, format="json", status=404); return; } + renderWith(data=resource, format="json", status=200); + } + + function create() { + resource = model("Resource").new(params.resource); + if (resource.save()) + renderWith(data=resource, format="json", status=201, location=urlFor(action="show", key=resource.key())); + else + renderWith(data={errors=resource.allErrors()}, format="json", status=422); + } + + function update() { + resource = model("Resource").findByKey(key=params.key); + if (!isObject(resource)) { renderWith(data={error="Not found"}, format="json", status=404); return; } + if (resource.update(params.resource)) + renderWith(data=resource, format="json", status=200); + else + renderWith(data={errors=resource.allErrors()}, format="json", status=422); + } + + function delete() { + resource = model("Resource").findByKey(key=params.key); + if (!isObject(resource)) { renderWith(data={error="Not found"}, format="json", status=404); return; } + resource.delete(); + renderWith(data={message="Deleted"}, format="json", status=204); + } + + private function requireApiAuth() { + var headers = getHTTPRequestData().headers; + if (!structKeyExists(headers, "Authorization")) { + renderWith(data={error="Unauthorized"}, format="json", status=401); abort; + } + var token = replace(headers.Authorization, "Bearer ", ""); + if (!isValidApiToken(token)) { + renderWith(data={error="Invalid token"}, format="json", status=401); abort; + } + } +} +``` + +**Status codes:** 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Validation Errors, 500 Server Error. + +--- + +## 3. Email Scaffold + +### Mailer Controller + +```cfm +component extends="Controller" { + function config() { + set(functionName="sendEmail", from="noreply@yourapp.com", layout="email", detectMultipart=true); + } + + function welcome(required user) { + sendEmail(to=arguments.user.email, subject="Welcome!", + template="mailer/welcome", user=arguments.user); + } + + function resetPassword(required user, required token) { + local.resetUrl = URLFor(route="passwordReset", token=arguments.token, onlyPath=false); + sendEmail(to=arguments.user.email, subject="Reset Your Password", + template="mailer/resetPassword", user=arguments.user, resetUrl=local.resetUrl); + } +} +``` + +### HTML Email Layout (views/mailer/layouts/email.cfm) + +```cfm + + + + + + + +

Your App

+
##contentForLayout()##
+ + + +``` + +### Plain Text Layout (views/mailer/layouts/email.txt.cfm) + +```cfm +==== YOUR APP ==== +##contentForLayout()## +==== (c) #Year(Now())# Your Company ==== +``` + +### Email Templates + +**Welcome** (views/mailer/welcome.cfm): +```cfm + + +

Welcome, ##user.firstName##!

+

Get Started

+
+``` + +**Password Reset** (views/mailer/resetPassword.cfm): +```cfm + + +

Reset Your Password

+

Reset Password

+

Or copy: ##resetUrl##

+

Expires in 1 hour.

+
+``` + +### SMTP Configuration (config/settings.cfm) + +```cfm + +set(functionName="sendEmail", server="smtp.gmail.com", port=587, + useTLS=true, from="noreply@yourapp.com", type="html", charset="utf-8"); +if (application.wheels.environment == "development") + set(functionName="sendEmail", debug=true, deliver=false); +if (application.wheels.environment == "production") + set(functionName="sendEmail", server=getEnv("SMTP_SERVER"), + username=getEnv("SMTP_USERNAME"), password=getEnv("SMTP_PASSWORD"), deliver=true); + +``` + +**SMTP providers:** Gmail (smtp.gmail.com:587), SendGrid (smtp.sendgrid.net:587, user="apikey"), Mailgun (smtp.mailgun.org:587), AWS SES (email-smtp.REGION.amazonaws.com:587). + +### Email Queue Pattern + +```cfm +function queueEmail(required struct emailData) { + model("EmailQueue").create(recipient=arguments.emailData.to, + subject=arguments.emailData.subject, data=serializeJSON(arguments.emailData), status="pending"); +} + +function processEmailQueue() { + pending = model("EmailQueue").findAll(where="status='pending'", order="createdAt", maxRows=50); + for (local.email in pending) { + try { + sendEmail(argumentCollection=deserializeJSON(local.email.data)); + local.email.update(status="sent", sentAt=now()); + } catch (any e) { local.email.update(status="failed", errorMessage=e.message); } + } +} +``` + +### Calling Mailer from Controllers + +```cfm +if (user.save()) { + controller("Mailer").welcome(user); + redirectTo(route="home", success="Account created!"); +} +``` + +--- + +## 4. Plugin Scaffold + +### Directory Structure + +``` +/plugins/YourPlugin/ +├── index.cfm # Entry point (required) +├── box.json # ForgeBox metadata +├── config/settings.cfm # Plugin config +├── db/migrate/ # Migrations (optional) +└── tests/PluginTest.cfc +``` + +### Plugin Entry Point (index.cfm) + +```cfm + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Plugin box.json + +```json +{ + "name": "YourPlugin", "slug": "your-plugin", "version": "1.0.0", + "type": "cfwheels-plugins", + "shortDescription": "Brief description", + "keywords": ["cfwheels", "plugin"], + "engines": [ + { "type": "lucee", "version": ">=5.0.0" }, + { "type": "adobe", "version": ">=2018.0.0" } + ], + "license": [{ "type": "Apache-2.0" }], + "ignore": ["**/.*", "tests"] +} +``` + +### Plugin Event Handlers + +```cfm + + + + + + + + + + +``` + +Available events: `onApplicationStart`, `onRequestStart`, `onRequestEnd`, `onSessionStart`, `onSessionEnd`, `onError`, `onMissingMethod`, `onMissingTemplate`. + +### Plugin Database Migration + +```cfm +component extends="wheels.migrator.Migration" { + function up() { + transaction { + t = createTable(name="plugin_data"); + t.string(columnNames="name"); + t.text(columnNames="data"); + t.timestamps(); + t.create(); + } + } + function down() { dropTable("plugin_data"); } +} +``` + +### Plugin Testing + +```cfm +component extends="wheels.Test" { + function testPluginMethodExists() { + assert("structKeyExists(variables.wheels, 'myPluginMethod')"); + } + function testPluginMethodReturnsCorrectly() { + result = myPluginMethod("test"); + assert("result == 'Plugin: test'"); + } +} +``` + +### ForgeBox Publishing + +```bash +box forgebox login && box forgebox publish +# Version bumps: box bump --major | --minor | --patch +``` + +**Best practices:** Prefix function names to avoid conflicts. Never overwrite core methods. Provide config via settings. Include tests, README, LICENSE. Use semantic versioning. diff --git a/.claude/skills/wheels-test-generator/SKILL.md b/.claude/skills/wheels-test-generator/SKILL.md deleted file mode 100755 index 6cccbce846..0000000000 --- a/.claude/skills/wheels-test-generator/SKILL.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -name: Wheels Test Generator -description: Generate TestBox BDD test specs for Wheels models, controllers, and integration tests. Use when creating tests for models (validations, associations), controllers (actions, filters), or integration workflows. Ensures comprehensive test coverage with proper setup/teardown and Wheels testing conventions. ---- - -# Wheels Test Generator - -## When to Use This Skill - -Activate automatically when: -- User requests to create tests/specs -- User wants to test a model, controller, or workflow -- User mentions: test, spec, TestBox, BDD, describe, it, expect -- After generating models/controllers (proactive testing) - -## Model Test Template - -```cfm -component extends="wheels.Test" { - - function setup() { - // Runs before each test - super.setup(); - model = model("Post").new(); - } - - function teardown() { - // Runs after each test - if (isObject(model) && model.isPersisted()) { - model.delete(); - } - super.teardown(); - } - - function testValidatesPresenceOfTitle() { - model.title = ""; - assert("!model.valid()"); - assert("model.hasErrors('title')"); - } - - function testHasManyComments() { - model = model("Post").create(title="Test", content="Content"); - comment = model("Comment").create(postId=model.id, content="Comment"); - - assert("model.comments().recordCount == 1"); - - model.delete(); // Cascade should delete comment - assert("!isObject(model('Comment').findByKey(comment.id))"); - } -} -``` - -## Controller Test Template - -```cfm -component extends="wheels.Test" { - - function setup() { - super.setup(); - params = {controller="posts", action="index"}; - } - - function testIndexLoadsAllPosts() { - controller = controller("Posts", params); - controller.processAction("index"); - - assert("isQuery(controller.posts)"); - } - - function testShowRequiresKey() { - params.action = "show"; - controller = controller("Posts", params); - // Should redirect due to missing key - } - - function testCreateWithValidData() { - params.action = "create"; - params.post = {title="Test", content="Content"}; - - controller = controller("Posts", params); - controller.processAction("create"); - - assert("flashKeyExists('success')"); - } -} -``` - -## Integration Test Template - -```cfm -component extends="wheels.Test" { - - function testCompletePostLifecycle() { - // Create - post = model("Post").create(title="Test", content="Content"); - assert("isObject(post) && post.isPersisted()"); - - // Update - post.update(title="Updated"); - assert("post.title == 'Updated'"); - - // Add comment - comment = model("Comment").create(postId=post.id, content="Comment"); - assert("post.comments().recordCount == 1"); - - // Delete (cascade) - post.delete(); - assert("!isObject(model('Comment').findByKey(comment.id))"); - } -} -``` - -## Browser-Based Integration Testing - -For end-to-end testing of user workflows, use MCP browser tools: - -### Basic Browser Test Flow -```javascript -// 1. Start server and navigate -mcp__wheels__wheels_server(action="status") - -// 2. Use available browser MCP (in order of preference): - -// Option A: Puppeteer MCP (preferred) -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts") -mcp__puppeteer__puppeteer_screenshot(name="posts-index") -mcp__puppeteer__puppeteer_click(selector="a[href*='new']") -mcp__puppeteer__puppeteer_screenshot(name="posts-new-form") - -// Option B: Playwright MCP -mcp__playwright__playwright_navigate(url="http://localhost:PORT/posts") -mcp__playwright__playwright_screenshot() - -// Option C: Browser MCP (fallback) -mcp__browsermcp__browser_navigate(url="http://localhost:PORT/posts") -mcp__browsermcp__browser_screenshot() -``` - -### Complete User Workflow Test -```javascript -// Test: Create new post -// 1. Navigate to new post form -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts/new") -mcp__puppeteer__puppeteer_screenshot(name="step1-form") - -// 2. Fill form fields -mcp__puppeteer__puppeteer_click(selector="input[name='post[title]']") -// Enter data... - -// 3. Submit and verify -mcp__puppeteer__puppeteer_click(selector="button[type='submit']") -mcp__puppeteer__puppeteer_screenshot(name="step2-created") - -// 4. Verify success message or redirect -// Check screenshot for success indicators -``` - -### Testing CRUD Operations -```javascript -// CREATE: Test form submission -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts/new") - -// READ: Test index and show pages -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts") -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts/1") - -// UPDATE: Test edit form -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/posts/1/edit") - -// DELETE: Test delete action -// Verify through index page -``` - -## 🚨 Test Database Setup (CRITICAL) - -### Issue: Test Environment Migrations - -**Problem:** Test environment uses `tests/populate.cfm` which may fail to run migrations properly, falling back to manual table creation. - -**Symptoms:** -- Tests fail with "table not found" errors -- Tests fail with "no primary key" errors -- Migrations work in development but not in test environment -- populate.cfm catches migrator errors and creates tables manually - -**Root Cause:** Test environment `populate.cfm` tries to run migrations via Migrator component but catches errors and falls back to manual SQL, which may not match migration schema exactly. - -### Solution: Update populate.cfm for New Tables - -When adding new tables/migrations, you MUST update `tests/populate.cfm` to include manual table creation: - -```cfm - - // Run Wheels migrations to set up test database schema - try { - migrator = createObject("component", "wheels.migrator.Migrator").init( - migratePath = application.wo.get("rootPath") & "app/migrator/migrations/", - datasourceName = application.wheels.dataSourceName - ); - migrator.migrateToVersion(); - } catch (any e) { - writeLog(file="application", text="Error running migrations: #e.message#"); - - // Fallback to manual table creation - try { - // Create each table manually with H2-compatible syntax - queryExecute("DROP TABLE IF EXISTS tablename", {}, {datasource: application.wheels.dataSourceName}); - - queryExecute(" - CREATE TABLE tablename ( - id INT IDENTITY PRIMARY KEY, -- H2 uses IDENTITY not AUTO_INCREMENT - columnName VARCHAR(255) NOT NULL, - createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - deletedAt TIMESTAMP - ) - ", {}, {datasource: application.wheels.dataSourceName}); - - // Add indexes - queryExecute("CREATE INDEX idx_tablename_column ON tablename(columnName)", - {}, {datasource: application.wheels.dataSourceName}); - - } catch (any e2) { - writeLog(file="application", text="Error creating tables manually: #e2.message#"); - } - } - -``` - -### H2 Database Syntax (CRITICAL) - -**When writing manual table creation for tests, use H2-compatible syntax:** - -```cfm -// ✅ CORRECT for H2: -id INT IDENTITY PRIMARY KEY - -// ❌ WRONG - causes "no primary key" errors: -id INT AUTO_INCREMENT PRIMARY KEY -id INTEGER AUTO_INCREMENT PRIMARY KEY -``` - -**Key H2 Differences:** -- Use `IDENTITY` not `AUTO_INCREMENT` for auto-increment columns -- Use `INT` not `INTEGER` for integer columns -- Use `VARCHAR(n)` not `VARCHAR2(n)` -- Use `TIMESTAMP` not `DATETIME` - -### Test Database Workflow - -**Option A: Manual Table Creation (Current)** -1. Update `tests/populate.cfm` with manual CREATE TABLE statements -2. Use H2-compatible syntax (IDENTITY for primary keys) -3. Match migration schema exactly -4. Include all indexes and constraints - -**Option B: Fix Migrator Integration (Future)** -1. Debug why migrator fails in test environment -2. Fix migration system to work with test database -3. Remove manual table creation fallback -4. Let migrations handle test database schema - -### Checklist for New Tables - -When adding new models/migrations: -- [ ] Create migration in `app/migrator/migrations/` -- [ ] Run migration in development: `wheels dbmigrate latest` -- [ ] Update `tests/populate.cfm` with manual H2 table creation -- [ ] Use `IDENTITY` for primary keys in test tables -- [ ] Match column names/types exactly between migration and populate.cfm -- [ ] Run tests to verify: `wheels test run` - -## Related Skills - -- **wheels-model-generator**: Creates models to test -- **wheels-controller-generator**: Creates controllers to test -- **wheels-debugging**: Use when tests fail - ---- - -**Generated by:** Wheels Test Generator Skill v1.1 diff --git a/.claude/skills/wheels-testing/SKILL.md b/.claude/skills/wheels-testing/SKILL.md new file mode 100644 index 0000000000..ba018dd7bf --- /dev/null +++ b/.claude/skills/wheels-testing/SKILL.md @@ -0,0 +1,308 @@ +--- +name: Wheels Testing & Debugging +description: Generate TestBox BDD tests for Wheels models, controllers, and integration workflows. Diagnose and fix common Wheels errors. Use when creating tests, running test suites, or troubleshooting failures. +--- + +# Wheels Testing & Debugging + +## Model Test Template + +```cfm +component extends="wheels.Test" { + function setup() { + super.setup(); + model = model("Post").new(); + } + function teardown() { + if (isObject(model) && model.isPersisted()) model.delete(); + super.teardown(); + } + + // Validation tests + function testValidatesPresenceOfTitle() { + model.title = ""; + assert("!model.valid()"); + assert("model.hasErrors('title')"); + } + function testValidatesUniquenessOfEmail() { + model("User").create(email="test@example.com", firstname="A", lastname="B"); + var dupe = model("User").new(email="test@example.com", firstname="C", lastname="D"); + assert("!dupe.valid()"); + } + function testValidatesFormatOfEmail() { + model.email = "not-an-email"; + assert("!model.valid()"); + } + + // Association tests + function testHasManyComments() { + model = model("Post").create(title="Test", content="Content"); + model("Comment").create(postId=model.id, content="A comment"); + assert("model.comments().recordCount == 1"); + } + function testBelongsToAuthor() { + var author = model("Author").create(name="Jane"); + model = model("Post").create(title="Test", authorId=author.id); + assert("isObject(model.author())"); + assert("model.author().name == 'Jane'"); + } + + // Custom method tests + function testFullNameMethod() { + var user = model("User").new(firstname="John", lastname="Doe"); + assert("user.fullName() == 'John Doe'"); + } + + // CRUD tests + function testCreateWithValidData() { + var post = model("Post").create(title="New", content="Body"); + assert("isObject(post) && post.isPersisted()"); + } + function testDeleteCascadesComments() { + model = model("Post").create(title="Test", content="Content"); + var comment = model("Comment").create(postId=model.id, content="Comment"); + model.delete(); + assert("!isObject(model('Comment').findByKey(comment.id))"); + } +} +``` + +## Controller Test Template + +```cfm +component extends="wheels.Test" { + function setup() { + super.setup(); + params = {controller="posts", action="index"}; + } + + function testIndexLoadsAllPosts() { + controller = controller("Posts", params); + controller.processAction("index"); + assert("isQuery(controller.posts)"); + } + function testShowFindsPost() { + var post = model("Post").create(title="Test", content="Body"); + params.action = "show"; + params.key = post.id; + controller = controller("Posts", params); + controller.processAction("show"); + assert("isObject(controller.post)"); + } + function testCreateWithValidData() { + params.action = "create"; + params.post = {title="Test", content="Content"}; + controller = controller("Posts", params); + controller.processAction("create"); + assert("flashKeyExists('success')"); + } + function testCreateWithInvalidData() { + params.action = "create"; + params.post = {title="", content=""}; + controller = controller("Posts", params); + controller.processAction("create"); + // Should render new form again, not redirect + } + function testRequiresAuthentication() { + params.action = "edit"; + params.key = 1; + controller = controller("Posts", params); + controller.processAction("edit"); + // Without session auth, should redirect + } +} +``` + +## Integration Test Template + +```cfm +component extends="wheels.Test" { + function testCompletePostLifecycle() { + // Create + var post = model("Post").create(title="Integration Test", content="Body"); + assert("isObject(post) && post.isPersisted()"); + // Read + var found = model("Post").findByKey(post.id); + assert("found.title == 'Integration Test'"); + // Update + found.update(title="Updated Title"); + var refreshed = model("Post").findByKey(post.id); + assert("refreshed.title == 'Updated Title'"); + // Delete + found.delete(); + assert("!isObject(model('Post').findByKey(post.id))"); + } + + function testAssociationWorkflow() { + var author = model("Author").create(name="Jane"); + var post = model("Post").create(title="Test", authorId=author.id); + model("Comment").create(postId=post.id, content="Great post"); + assert("post.author().name == 'Jane'"); + assert("post.comments().recordCount == 1"); + post.delete(); + } +} +``` + +## Test Setup: beforeAll/afterAll Patterns + +```cfm +component extends="wheels.Test" { + function beforeAll() { + // Runs once before all tests - seed shared data + variables.testAuthor = model("Author").create(name="Test Author"); + } + function afterAll() { + // Runs once after all tests - cleanup shared data + if (isObject(variables.testAuthor)) variables.testAuthor.delete(); + } + function setup() { + super.setup(); + // Runs before EACH test - fresh per-test state + variables.post = ""; + } + function teardown() { + if (isObject(variables.post) && variables.post.isPersisted()) variables.post.delete(); + super.teardown(); + } +} +``` + +## H2 Test Database Syntax + +Test environment uses H2. Use H2-compatible syntax in `tests/populate.cfm`: + +```sql +-- Primary key (CRITICAL - wrong syntax causes "no primary key" errors) +id INT IDENTITY PRIMARY KEY -- CORRECT for H2 +id INT AUTO_INCREMENT PRIMARY KEY -- WRONG (MySQL syntax) + +-- H2 column type mappings +VARCHAR(255) -- not VARCHAR2 +TIMESTAMP -- not DATETIME +INT -- not INTEGER +BOOLEAN -- supported natively +TEXT -- for long text +DECIMAL(10,2) -- for decimals +``` + +Example `tests/populate.cfm` table creation: +```cfm +queryExecute("DROP TABLE IF EXISTS posts", {}, {datasource: application.wheels.dataSourceName}); +queryExecute(" + CREATE TABLE posts ( + id INT IDENTITY PRIMARY KEY, + title VARCHAR(255) NOT NULL, + content TEXT, + authorId INT, + createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deletedAt TIMESTAMP + ) +", {}, {datasource: application.wheels.dataSourceName}); +``` + +## Running Tests + +**MCP tools (preferred when .mcp.json exists):** +```javascript +mcp__wheels__wheels_test() // Run all tests +mcp__wheels__wheels_test(target="models") // Model tests only +mcp__wheels__wheels_test(target="controllers") // Controller tests only +``` + +**CLI fallback:** `wheels test run` + +**Test file organization:** +``` +tests/ + Test.cfc # Base test component + models/ # Model tests (one per model) + controllers/ # Controller tests (one per controller) + integration/ # End-to-end workflow tests +``` + +**New table checklist:** +1. Create migration in `app/migrator/migrations/` +2. Run migration: `mcp__wheels__wheels_migrate(action="latest")` +3. Update `tests/populate.cfm` with H2-compatible CREATE TABLE +4. Use `IDENTITY` for primary keys, match column names/types exactly +5. Run tests to verify + +## Debugging Guide: Common Errors + +### "No matching function [RENDERPAGE] found" +Wheels uses `renderView()` not `renderPage()`: +```cfm +renderPage(action="new") // WRONG +renderView(action="new") // CORRECT +``` + +### "Missing argument name" Error +Mixed positional and named arguments. Use consistent style: +```cfm +hasMany("comments", dependent="delete") // WRONG - mixed +hasMany(name="comments", dependent="delete") // CORRECT - all named +hasMany("comments") // CORRECT - all positional + +.resources("sessions", only="new,create") // WRONG +.resources(name="sessions", only="new,create") // CORRECT +``` + +### "key [onCreate,onUpdate] doesn't exist" +Comma-separated `when` parameter. Validations run on create and update by default: +```cfm +validatesConfirmationOf(properties="password", when="onCreate,onUpdate") // WRONG +validatesConfirmationOf(properties="password") // CORRECT +``` + +### "Can't cast Object type [Query] to [Array]" +Associations return queries, not arrays: +```cfm +ArrayLen(post.comments()) // WRONG - returns query +post.comments().recordCount // CORRECT +``` + +### "Association not found" Error +1. Association not defined in model `config()` -- add `hasMany()`/`belongsTo()` +2. Typo in association name -- verify spelling matches model name +3. Model file missing or misnamed -- check `/app/models/` + +### "Table not found" Error +1. Migration not run -- `mcp__wheels__wheels_migrate(action="latest")` +2. Table name mismatch -- Wheels pluralizes model names (User -> users) +3. Test environment -- update `tests/populate.cfm` with CREATE TABLE + +### "Column not found" Error +1. Column missing -- add via migration +2. Spelling/case mismatch -- check exact column name in database +3. Migration not applied -- run pending migrations + +### "No primary key" in Tests +H2 requires `IDENTITY`, not `AUTO_INCREMENT`: +```sql +id INT IDENTITY PRIMARY KEY -- CORRECT for H2 +id INT AUTO_INCREMENT PRIMARY KEY -- WRONG for H2 +``` + +## Debugging Tools & Strategies + +**Enable debug output:** +```cfm +// config/settings.cfm +set(showDebugInformation=true); +set(showErrorInformation=true); +``` + +**Inspect variables:** `` + +**Check SQL:** Wheels logs all SQL to debug output. Red queries indicate errors. + +**Debugging strategy:** +1. Read the full error message -- Wheels errors are descriptive +2. Check model `config()` for association/validation definitions +3. Verify migrations: `mcp__wheels__wheels_migrate(action="info")` +4. Use `?reload=true` after config changes +5. Check debug footer for route information +6. Test associations and routes in isolation before combining +7. Start simple, add complexity incrementally diff --git a/.claude/skills/wheels-view-generator/SKILL.md b/.claude/skills/wheels-view-generator/SKILL.md deleted file mode 100755 index 41dd7e0638..0000000000 --- a/.claude/skills/wheels-view-generator/SKILL.md +++ /dev/null @@ -1,703 +0,0 @@ ---- -name: Wheels View Generator -description: Generate Wheels view templates with proper query handling, form helpers, and association display. Use when creating or modifying views, forms, layouts, or partials. Prevents common view errors like query/array confusion and incorrect form helper usage. Handles index views, show views, form views, and layouts with proper CFML syntax. ---- - -# Wheels View Generator - -## When to Use This Skill - -Activate automatically when: -- User requests to create a view (e.g., "create an index view for posts") -- User wants to create forms -- User needs to display associated data -- User is creating layouts or partials -- User mentions: view, template, form, layout, partial, display, list, show - -## Critical Anti-Patterns to Prevent - -### ❌ ANTI-PATTERN 1: Query/Array Confusion - -**Wheels associations return QUERIES, not arrays!** - -**WRONG:** -```cfm - ❌ - ❌ -``` - -**CORRECT:** -```cfm - ✅ - ✅ -``` - -### ❌ ANTI-PATTERN 2: Association Access Inside Query Loops - -**WRONG:** -```cfm - -

#posts.comments().recordCount# comments

❌ Fails! -
-``` - -**CORRECT:** -```cfm - - -

#postComments.recordCount# comments

✅ Works! -
-``` - -### ❌ ANTI-PATTERN 3: Non-Existent Form Helpers - -**Wheels doesn't have these helpers:** -```cfm -#emailField(...)# ❌ Doesn't exist -#passwordField(...)# ❌ Doesn't exist -#numberField(...)# ❌ Doesn't exist -``` - -**Use textField() with type attribute:** -```cfm -#textField(objectName="user", property="email", type="email")# ✅ -#textField(objectName="user", property="password", type="password")# ✅ -#textField(objectName="user", property="age", type="number")# ✅ -``` - -### ❌ ANTI-PATTERN 4: HTML in linkTo() Text - -**linkTo() HTML-encodes the text parameter by default for security:** -```cfm -❌ WRONG - HTML will be escaped and displayed as text: -#linkTo(text="", controller="home", action="index")# -``` - -**Use manual anchor tag with urlFor() for HTML content:** -```cfm -✅ CORRECT - HTML renders properly: - - - -``` - -### ❌ ANTI-PATTERN 5: Assuming allErrors() Returns Array - -**allErrors(propertyName) can return string OR array:** -```cfm -❌ WRONG - Assumes always array: - -
  • #error#
  • -
    -``` - -**✅ BEST PRACTICE - Inline Ternary (Task 4 Pattern):** -```cfm - - - -

    #isArray(emailErrors) ? emailErrors[1] : emailErrors#

    -
    - -### ❌ ANTI-PATTERN 6: Wrong startFormTag() Pattern for Update Forms - -**CRITICAL: Using action="update" without route causes forms to submit as GET:** -```cfm -❌ WRONG - Submits as GET, doesn't reach update action: -#startFormTag(action="update", method="patch")# - -❌ ALSO WRONG - Missing key parameter: -#startFormTag(action="update", key=user.id, method="patch")# -``` - -**✅ CORRECT - Use route with key for RESTful updates:** -```cfm - -#startFormTag(action="create", method="post")# - - -#startFormTag(route="user", key=user.id, method="patch")# - - - - #startFormTag(action="create", method="post")# - - #startFormTag(route="user", key=user.id, method="patch")# - -``` - -**Why This Matters:** -- `action="update"` tries to route to /controller/update (wrong) -- `route="user"` with `key` routes to /users/[id] (correct RESTful pattern) -- Without proper route+key, form submits as GET to wrong endpoint -- PATCH method requires correct route to work properly -``` - -**✅ ALTERNATIVE - Explicit Type Check (for multiple errors):** -```cfm - - - -
  • #error#
  • -
    - -
  • #propertyErrors#
  • -
    -``` - -**📝 Production Note (Task 4):** -The inline ternary pattern is cleaner for form field errors where you only need to display the first error message. - -## 🚨 Production-Tested Best Practices - -### 1. Association Access in Query Loops (CRITICAL) - -**❌ WRONG - Associations Don't Work on Query Columns:** -```cfm - - #tweets.user()# ❌ FAILS - tweets is a query, not an object - #tweets.likesCount()# ❌ FAILS - no such method on query - -``` - -**✅ CORRECT - Load Object First:** -```cfm - - - ✅ Works! -

    By: #tweetUser.username#

    -

    Likes: #tweetObj.likesCount#

    -
    -``` - -**✅ BETTER - Preload with include (Prevents N+1):** -```cfm - -tweets = model("Tweet").findAll(include="user", order="createdAt DESC"); - - - - -

    By: #tweets.username#

    -

    Tweet: #tweets.content#

    -
    -``` - -### 2. Checking if Query Has Records - -```cfm - - - ... - - - - ❌ Error! -``` - -### 3. Counter Access Patterns - -**Direct column access (if eager loaded):** -```cfm -tweets = model("Tweet").findAll(select="id,content,likesCount"); - -

    #tweets.likesCount# likes

    ✅ Direct access -
    -``` - -**Association count (if not loaded):** -```cfm - - - ✅ Count association -

    #likeCount# likes

    -
    -``` - -### 4. Boolean Checks in Views - -```cfm - - - - - - // Redundant - // Fails if bio doesn't exist -``` - -### 5. Date Formatting - -```cfm - -#dateFormat(tweet.createdAt, "mmm dd, yyyy")# -#timeFormat(tweet.createdAt, "h:mm tt")# - - -#tweet.timeAgo()# // "5m", "2h", "3d" -``` - -### 6. Loop Query vs Loop Array - -```cfm - - - #tweets.content# - - - - ❌ Error! -``` - -## Index View Template (List View) - -```cfm - - - -#contentFor(pageTitle="Resources")# - -
    -
    -

    Resources

    -
    - #linkTo(text="New Resource", action="new", class="btn btn-primary")# -
    -
    - - -
    - -
    -

    - #linkTo(text=resources.title, action="show", key=resources.id)# -

    - - -

    #left(resources.description, 200)#...

    - - - -
    - #resourceAssoc.recordCount# items - #dateFormat(resources.createdAt, "mmm dd, yyyy")# -
    - -
    - #linkTo(text="View", action="show", key=resources.id, class="btn btn-sm")# - #linkTo(text="Edit", action="edit", key=resources.id, class="btn btn-sm")# -
    -
    -
    -
    - - - #paginationLinks(prependToLink="page=")# - -
    -

    No resources found.

    - #linkTo(text="Create First Resource", action="new", class="btn btn-primary")# -
    -
    -
    - -
    -``` - -## Show View Template (Detail View) - -```cfm - - - - -#contentFor(pageTitle=resource.title)# - -
    -
    -

    #resource.title#

    -
    - #linkTo(text="Edit", action="edit", key=resource.id, class="btn")# - #linkTo( - text="Delete", - action="delete", - key=resource.id, - method="delete", - confirm="Are you sure?", - class="btn btn-danger" - )# - #linkTo(text="Back to List", action="index", class="btn")# -
    -
    - -
    - -
    - #resource.description# -
    - - - -
    - - -
    -

    Associated Items (#associations.recordCount#)

    - - -
      - -
    • - #associations.name# - #dateFormat(associations.createdAt, "mmm dd")# -
    • -
      -
    - -

    No associated items.

    -
    -
    -
    - -
    -``` - -## Form View Template (New/Edit) - -```cfm - - - -#contentFor(pageTitle=resource.isNew() ? "New Resource" : "Edit Resource")# - -
    -

    #resource.isNew() ? "Create" : "Edit"# Resource

    - - - -
    -

    Please correct the following errors:

    -
      - - -
    • #errorMessage#
    • -
      -
      -
    -
    -
    - - - - #startFormTag(action="create", method="post")# - - #startFormTag(route="resource", key=resource.id, method="patch")# - - - -
    - - #textField(objectName="resource", property="title", label=false, class="form-control")# - - - #isArray(titleErrors) ? titleErrors[1] : titleErrors# - -
    - - -
    - - #textArea(objectName="resource", property="description", label=false, rows=6, class="form-control")# - - - #isArray(descErrors) ? descErrors[1] : descErrors# - -
    - - -
    - - #textField(objectName="resource", property="email", type="email", label=false, class="form-control")# - - - #isArray(emailErrors) ? emailErrors[1] : emailErrors# - -
    - - -
    - - #textField(objectName="resource", property="price", type="number", step="0.01", label=false, class="form-control")# -
    - - -
    - - #dateSelect(objectName="resource", property="publishedDate", label=false, class="form-control")# -
    - - -
    - - #select( - objectName="resource", - property="status", - options="draft,published,archived", - label=false, - class="form-control" - )# -
    - - -
    - -
    - - -
    - - #select( - objectName="resource", - property="categoryId", - options=model("Category").findAll(), - valueField="id", - textField="name", - includeBlank="-- Select Category --", - label=false, - class="form-control" - )# -
    - - -
    - #submitTag(value=resource.isNew() ? "Create Resource" : "Update Resource", class="btn btn-primary")# - #linkTo(text="Cancel", action="index", class="btn")# -
    - - #endFormTag()# -
    - -
    -``` - -## Layout Template - -```cfm - - - - - - #contentFor("pageTitle")# - My App - - - #styleSheetLinkTag("application")# - - - #csrfMetaTags()# - - - - - - - -
    - #flash("success")# -
    -
    - -
    - #flash("error")# -
    -
    - -
    - #flash("notice")# -
    -
    - - -
    - #includeContent()# -
    - - -
    -
    -

    © #year(now())# My App. All rights reserved.

    -
    -
    - - - #javaScriptIncludeTag("application")# - - -``` - -## Partial Template - -```cfm - - - -
    -

    #linkTo(text=resource.title, action="show", key=resource.id)#

    -

    #resource.excerpt()#

    -
    - #dateFormat(resource.createdAt, "mmm dd, yyyy")# -
    -
    -
    - - - -``` - -## Form Helper Reference - -### Text Inputs - -```cfm - -#textField(objectName="user", property="name")# - - -#textField(objectName="user", property="email", type="email")# -#textField(objectName="user", property="password", type="password")# -#textField(objectName="user", property="age", type="number")# -#textField(objectName="user", property="website", type="url")# - - -#textField( - objectName="user", - property="name", - class="form-control", - placeholder="Enter your name", - maxlength=100, - required=true -)# -``` - -### Textarea - -```cfm -#textArea(objectName="post", property="content", rows=10, cols=50)# -``` - -### Select Dropdown - -```cfm - -#select(objectName="user", property="role", options="user,admin,moderator")# - - -#select( - objectName="post", - property="categoryId", - options=model("Category").findAll(), - valueField="id", - textField="name", - includeBlank="-- Select Category --" -)# -``` - -### Checkboxes and Radio Buttons - -```cfm - -#checkBox(objectName="user", property="active")# - - -#radioButton(objectName="user", property="gender", tagValue="male")# Male -#radioButton(objectName="user", property="gender", tagValue="female")# Female -``` - -### Date/Time Selects - -```cfm - -#dateSelect(objectName="event", property="eventDate")# - - -#timeSelect(objectName="event", property="eventTime")# - - -#dateTimeSelect(objectName="event", property="eventDateTime")# -``` - -## Link Helper Reference - -```cfm - -#linkTo(text="View", action="show", key=resource.id)# - - -#linkTo(text="Home", controller="home", action="index")# - - -#linkTo(text="Delete", action="delete", key=resource.id, method="delete", confirm="Are you sure?")# - - -#linkTo(text="Wheels Docs", href="https://wheels.dev", target="_blank")# - - -#linkTo(text="Edit", action="edit", key=resource.id, class="btn btn-primary", data-turbo="false")# -``` - -## Implementation Checklist - -When generating a view: - -- [ ] Use `` to declare expected variables -- [ ] Use `` blocks for dynamic content -- [ ] Use `.recordCount` for query counts (not ArrayLen) -- [ ] Use `` for query iteration -- [ ] Handle association access correctly in loops -- [ ] Use textField() with type attribute (not emailField, etc.) -- [ ] Display validation errors for each field -- [ ] Include CSRF protection in forms (automatic with startFormTag) -- [ ] Add flash message displays -- [ ] Use contentFor() to set page titles -- [ ] Provide empty state messages when no records - -## Related Skills - -- **wheels-anti-pattern-detector**: Validates view code -- **wheels-controller-generator**: Creates controllers that supply view data -- **wheels-model-generator**: Creates models displayed in views - ---- - -**Generated by:** Wheels View Generator Skill v1.0 -**Framework:** CFWheels 3.0+ -**Last Updated:** 2025-10-20 diff --git a/.opencode/command/wheels_execute.md b/.opencode/command/wheels_execute.md deleted file mode 100755 index 1db96a6c92..0000000000 --- a/.opencode/command/wheels_execute.md +++ /dev/null @@ -1,619 +0,0 @@ -# /wheels_execute - Comprehensive Wheels Development Workflow - -## Description -Execute a complete, systematic Wheels development workflow that implements features with professional quality, comprehensive testing, and bulletproof error prevention. - -## Usage -``` -/wheels_execute [task_description] -``` - -## Examples -``` -/wheels_execute create a blog with posts and comments -/wheels_execute add user authentication to the application -/wheels_execute build an e-commerce product catalog with shopping cart -/wheels_execute create admin dashboard for user management -/wheels_execute implement contact form with email notifications -``` - -## Workflow Overview - -The `/wheels_execute` command implements a comprehensive 7-phase development workflow: - -1. **Pre-Flight Documentation Loading** - Systematically load relevant patterns from `.ai` folder -2. **Intelligent Analysis & Planning** - Parse requirements and create detailed implementation plan -3. **Template-Driven Implementation** - Generate code using established patterns with error recovery -4. **TestBox BDD Test Suite Creation** - Write comprehensive BDD tests before marking complete -5. **Multi-Level Testing Execution** - Run unit tests, integration tests, and validation -6. **Comprehensive Browser Testing** - Test every button, form, and link automatically -7. **Quality Assurance & Reporting** - Anti-pattern detection and final validation - -## Phase Details - -### Phase 1: Pre-Flight Documentation Loading (2-3 minutes) -- **Critical Error Prevention**: Always load `common-errors.md` and `validation-templates.md` first -- **Smart Documentation Discovery**: Analyze task type and load relevant `.ai` documentation -- **Project Context Loading**: Understand existing codebase patterns and conventions -- **Pattern Recognition**: Detect argument styles and naming conventions already in use - -### Phase 2: Intelligent Analysis & Planning (3-5 minutes) -- **Requirement Analysis**: Parse natural language into specific Wheels components -- **Component Mapping**: Identify models, controllers, views, migrations needed -- **Dependency Analysis**: Determine implementation order and resolve conflicts -- **Browser Test Planning**: Plan comprehensive user flow testing scenarios -- **Risk Assessment**: Identify potential issues and mitigation strategies - -### Phase 3: Template-Driven Implementation (5-15 minutes) -- **Code Generation**: Use templates from `.ai/wheels/snippets/` as starting points -- **Incremental Validation**: Validate each component after generation -- **Error Recovery**: Intelligent fallbacks when generation fails -- **Consistency Enforcement**: Ensure patterns match existing codebase -- **Security Integration**: Add CSRF protection, validation, authentication - -### Phase 4: TestBox BDD Test Suite Creation (10-20 minutes) -- **Model Tests**: Write BDD specs for all model functionality, validations, and associations -- **Controller Tests**: Write BDD specs for all controller actions and security filters -- **Integration Tests**: Write BDD specs for complete user workflows and CRUD operations -- **Test Data Setup**: Create fixtures and test data for comprehensive testing -- **Validation Testing**: Write BDD specs for all form validation scenarios -- **Security Testing**: Write BDD specs for authentication, authorization, and CSRF protection - -### Phase 5: Multi-Level Testing Execution (3-8 minutes) -- **Unit Test Execution**: Run all model and controller BDD specs -- **Integration Test Execution**: Run all workflow and CRUD BDD specs -- **Migration Testing**: Verify database changes work correctly -- **Test Coverage Analysis**: Ensure all code paths are tested -- **Test Failure Resolution**: Fix any failing tests before proceeding - -### Phase 6: Comprehensive Browser Testing (5-10 minutes) -- **Server Verification**: Ensure development server is running -- **Navigation Testing**: Test all menu links, buttons, and navigation paths -- **CRUD Flow Testing**: Test complete create, read, update, delete operations -- **Form Testing**: Submit all forms, test validation scenarios -- **Interactive Testing**: Test JavaScript, Alpine.js, HTMX functionality -- **Responsive Testing**: Validate mobile, tablet, desktop layouts -- **Error Scenario Testing**: Test 404s, validation failures, edge cases - -### Phase 7: Quality Assurance & Reporting (2-3 minutes) -- **Anti-Pattern Detection**: Scan for mixed arguments, query/array confusion -- **Security Review**: Verify CSRF, authentication, input validation -- **Performance Analysis**: Check for N+1 queries, optimization opportunities -- **Documentation Compliance**: Validate against `.ai` documentation patterns -- **Test Coverage Report**: Generate detailed test coverage analysis -- **Comprehensive Reporting**: Generate detailed results with screenshots and test results - -## Anti-Pattern Prevention - -The workflow specifically prevents the two most common Wheels errors: - -### ❌ Mixed Argument Styles (PREVENTED) -```cfm -// BAD - will cause "Missing argument name" errors -hasMany("comments", dependent="delete"); -model("Post").findByKey(params.key, include="comments"); -``` - -### ✅ Consistent Argument Styles (ENFORCED) -```cfm -// GOOD - all named arguments -hasMany(name="comments", dependent="delete"); -model("Post").findByKey(key=params.key, include="comments"); - -// ALSO GOOD - all positional arguments -hasMany("comments"); -model("Post").findByKey(params.key); -``` - -### ❌ Query/Array Confusion (PREVENTED) -```cfm -// BAD - ArrayLen() on query objects - - -``` - -### ✅ Proper Query Handling (ENFORCED) -```cfm -// GOOD - use .recordCount for queries - - -``` - -## Success Criteria - -A feature is only considered complete when ALL of the following are true: -- [ ] ✅ All relevant `.ai` documentation was consulted -- [ ] ✅ No anti-patterns detected in generated code -- [ ] ✅ **Comprehensive TestBox BDD test suite written and passing** -- [ ] ✅ **All model BDD specs pass (validations, associations, methods)** -- [ ] ✅ **All controller BDD specs pass (actions, filters, security)** -- [ ] ✅ **All integration BDD specs pass (user workflows, CRUD)** -- [ ] ✅ **Test coverage >= 90% for all components** -- [ ] ✅ All browser tests pass -- [ ] ✅ Every button, form, and link has been tested -- [ ] ✅ Responsive design works on mobile, tablet, desktop -- [ ] ✅ Security validations are in place -- [ ] ✅ Performance is acceptable -- [ ] ✅ Error scenarios are handled properly -- [ ] ✅ Screenshot evidence exists for all user flows -- [ ] ✅ Implementation follows Wheels conventions - -## Browser Testing Coverage - -The workflow automatically tests: - -### Navigation Testing -- Homepage load and layout -- All menu links and navigation paths -- Breadcrumb navigation -- Footer links and utility pages - -### CRUD Operations Testing -- Index pages (list views) -- Show pages (detail views) -- New/Create forms and submission -- Edit/Update forms and submission -- Delete actions and confirmations - -### Form Validation Testing -- Empty form submissions (should show errors) -- Partial form submissions -- Invalid data submissions -- Complete valid form submissions -- CSRF protection verification - -### Interactive Elements Testing -- JavaScript functionality -- Alpine.js components and interactions -- HTMX requests and responses -- Modal dialogs and dropdowns -- Dynamic content updates - -### Responsive Design Testing -- Mobile viewport (375x667) -- Tablet viewport (768x1024) -- Desktop viewport (1920x1080) -- Wide screen viewport (2560x1440) -- Mobile navigation (hamburger menus) - -### Error Scenario Testing -- 404 pages for nonexistent resources -- Authentication redirects -- Authorization failures -- Validation error displays -- Server error handling - -## Quality Gates - -### Automatic Rejection Criteria -Code will be automatically rejected if: -- Any mixed argument styles are detected -- Any `ArrayLen()` calls on model associations exist -- **Any TestBox BDD spec fails** -- **Test coverage is below 90%** -- **Missing BDD specs for any component** -- Any browser test fails -- Any security check fails -- Any anti-pattern is detected -- Routes don't follow RESTful conventions - -### Performance Requirements -- Pages must load within 3 seconds -- Forms must submit within 2 seconds -- No N+1 query patterns allowed -- Database queries must be optimized - -### Security Requirements -- CSRF protection must be enabled -- All forms must include CSRF tokens -- Authentication filters must be present -- Input validation must be implemented -- SQL injection prevention must be verified - -## TestBox BDD Testing Requirements - -### Mandatory BDD Test Structure - -Every component MUST have comprehensive TestBox BDD specs using the following structure: - -#### Model Specs (`/tests/specs/models/`) -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - // Setup database and test environment - application.testbox = new testbox.system.TestBox(); - } - - function afterAll() { - // Cleanup test data - } - - function run() { - describe("Post Model", function() { - - beforeEach(function() { - variables.post = model("Post").new(); - }); - - afterEach(function() { - if (isObject(variables.post) && variables.post.isPersisted()) { - variables.post.delete(); - } - }); - - describe("Validations", function() { - it("should require title", function() { - variables.post.title = ""; - expect(variables.post.valid()).toBeFalse(); - expect(variables.post.allErrors()).toHaveKey("title"); - }); - - it("should require content", function() { - variables.post.content = ""; - expect(variables.post.valid()).toBeFalse(); - expect(variables.post.allErrors()).toHaveKey("content"); - }); - - it("should require unique slug", function() { - var existingPost = model("Post").create({ - title: "Test Post", - content: "Test content", - slug: "test-slug", - published: false - }); - - variables.post.slug = "test-slug"; - expect(variables.post.valid()).toBeFalse(); - expect(variables.post.allErrors()).toHaveKey("slug"); - - existingPost.delete(); - }); - }); - - describe("Associations", function() { - it("should have many comments", function() { - expect(variables.post.comments()).toBeQuery(); - }); - - it("should delete associated comments", function() { - var savedPost = model("Post").create({ - title: "Test Post", - content: "Test content", - published: false - }); - - var comment = model("Comment").create({ - content: "Test comment", - authorName: "Test Author", - authorEmail: "test@example.com", - postId: savedPost.id - }); - - expect(savedPost.comments().recordCount).toBe(1); - savedPost.delete(); - expect(model("Comment").findByKey(comment.id)).toBeFalse(); - }); - }); - - describe("Methods", function() { - it("should generate excerpt", function() { - variables.post.content = "

    This is a long content that should be truncated at some point for the excerpt.

    "; - expect(len(variables.post.excerpt(20))).toBeLTE(23); // 20 + "..." - }); - - it("should auto-generate slug from title", function() { - variables.post.title = "This is a Test Title!"; - variables.post.setSlugAndPublishDate(); - expect(variables.post.slug).toBe("this-is-a-test-title"); - }); - }); - }); - } -} -``` - -#### Controller Specs (`/tests/specs/controllers/`) -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - application.testbox = new testbox.system.TestBox(); - } - - function run() { - describe("Posts Controller", function() { - - beforeEach(function() { - // Setup test data - variables.testPost = model("Post").create({ - title: "Test Post", - content: "Test content for controller testing", - published: true, - publishedAt: now() - }); - }); - - afterEach(function() { - if (isObject(variables.testPost)) { - variables.testPost.delete(); - } - }); - - describe("index action", function() { - it("should load published posts", function() { - var controller = controller("Posts"); - controller.index(); - - expect(controller.posts).toBeQuery(); - expect(controller.posts.recordCount).toBeGTE(1); - }); - - it("should order posts by publishedAt DESC", function() { - var newerPost = model("Post").create({ - title: "Newer Post", - content: "Newer content", - published: true, - publishedAt: dateAdd("h", 1, now()) - }); - - var controller = controller("Posts"); - controller.index(); - - expect(controller.posts.title[1]).toBe("Newer Post"); - newerPost.delete(); - }); - }); - - describe("show action", function() { - it("should load post and comments", function() { - var controller = controller("Posts"); - controller.params.key = variables.testPost.id; - controller.show(); - - expect(controller.post.id).toBe(variables.testPost.id); - expect(controller.comments).toBeQuery(); - }); - }); - - describe("create action", function() { - it("should create valid post", function() { - var controller = controller("Posts"); - controller.params.post = { - title: "New Test Post", - content: "New test content", - published: true - }; - - var initialCount = model("Post").count(); - controller.create(); - - expect(model("Post").count()).toBe(initialCount + 1); - - // Cleanup - var newPost = model("Post").findOne(where="title = 'New Test Post'"); - if (isObject(newPost)) { - newPost.delete(); - } - }); - - it("should handle validation errors", function() { - var controller = controller("Posts"); - controller.params.post = { - title: "", // Invalid - empty title - content: "Test content" - }; - - controller.create(); - expect(controller.post.hasErrors()).toBeTrue(); - }); - }); - }); - } -} -``` - -#### Integration Specs (`/tests/specs/integration/`) -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Blog Workflow Integration", function() { - - beforeEach(function() { - // Setup clean test environment - }); - - afterEach(function() { - // Cleanup test data - }); - - describe("Complete post lifecycle", function() { - it("should create, publish, and delete post", function() { - // Create post - var post = model("Post").create({ - title: "Integration Test Post", - content: "Integration test content", - published: false - }); - - expect(post.isNew()).toBeFalse(); - expect(post.published).toBeFalse(); - - // Publish post - post.update({published: true, publishedAt: now()}); - expect(post.published).toBeTrue(); - - // Add comment - var comment = model("Comment").create({ - content: "Integration test comment", - authorName: "Test Author", - authorEmail: "test@example.com", - postId: post.id - }); - - expect(post.comments().recordCount).toBe(1); - - // Delete post (should cascade delete comments) - post.delete(); - expect(model("Comment").findByKey(comment.id)).toBeFalse(); - }); - }); - - describe("Form validation workflow", function() { - it("should prevent invalid post creation", function() { - var post = model("Post").new({ - title: "", // Invalid - content: "x" // Too short - }); - - expect(post.save()).toBeFalse(); - expect(post.allErrors()).toHaveKey("title"); - expect(post.allErrors()).toHaveKey("content"); - }); - }); - }); - } -} -``` - -### Test Execution Requirements - -#### Mandatory Test Commands -All tests MUST be executed and pass before completion: - -```bash -# Run all model specs -wheels test model --reporter=json - -# Run all controller specs -wheels test controller --reporter=json - -# Run all integration specs -wheels test integration --reporter=json - -# Run complete test suite with coverage -wheels test all --coverage --reporter=json -``` - -#### Test Coverage Requirements -- **Models**: 100% coverage of all public methods, validations, and associations -- **Controllers**: 100% coverage of all actions and filters -- **Integration**: 90% coverage of complete user workflows -- **Overall**: Minimum 90% total coverage across all components - -#### Test Data Management -- Use TestBox's `beforeEach()` and `afterEach()` for test isolation -- Create test fixtures for complex scenarios -- Always clean up test data to prevent test pollution -- Use database transactions for faster test execution - -## Error Recovery System - -When errors occur during any phase: - -1. **Identify Error Type**: Syntax, logic, pattern, or security error -2. **Load Recovery Documentation**: Load relevant `.ai` documentation for the error -3. **Apply Documented Solution**: Use established patterns from documentation -4. **Retry Operation**: Attempt the operation with corrected approach -5. **Log Pattern**: Document the error pattern for future prevention - -### Common Recovery Flows - -#### Mixed Argument Error Recovery -``` -Error: "Missing argument name" detected -→ Load: .ai/wheels/troubleshooting/common-errors.md -→ Fix: Convert to consistent argument style -→ Retry: Code generation with corrected pattern -→ Validate: Syntax check passes -``` - -#### Query/Array Confusion Recovery -``` -Error: ArrayLen() on query object detected -→ Load: .ai/wheels/models/data-handling.md -→ Fix: Use .recordCount and proper loop syntax -→ Retry: View generation with correct patterns -→ Validate: Browser test confirms functionality -``` - -#### TestBox BDD Test Failure Recovery -``` -Error: BDD specs failing or missing -→ Load: .ai/wheels/testing/ documentation -→ Fix: Write comprehensive BDD specs for all components -→ Retry: Run complete test suite -→ Validate: All tests pass with 90%+ coverage -``` - -#### Test Coverage Insufficient Recovery -``` -Error: Test coverage below 90% -→ Analyze: Identify untested code paths -→ Fix: Add BDD specs for missing scenarios -→ Retry: Run test suite with coverage analysis -→ Validate: Coverage meets minimum requirements -``` - -## Implementation Strategy - -### Documentation Loading Strategy -1. **Universal Critical Documentation** (always loaded first): - - `.ai/wheels/troubleshooting/common-errors.md` - - `.ai/wheels/patterns/validation-templates.md` - - `.ai/wheels/workflows/pre-implementation.md` - -2. **Component-Specific Documentation** (loaded based on task analysis): - - Models: `.ai/wheels/models/architecture.md`, `associations.md`, `validations.md` - - Controllers: `.ai/wheels/controllers/architecture.md`, `rendering.md`, `filters.md` - - Views: `.ai/wheels/views/data-handling.md`, `architecture.md`, `forms.md` - - Migrations: `.ai/wheels/database/migrations/creating-migrations.md` - -3. **Feature-Specific Documentation** (loaded as needed): - - Authentication: `.ai/wheels/models/user-authentication.md` - - Security: `.ai/wheels/security/csrf-protection.md` - - Forms: `.ai/wheels/views/helpers/forms.md` - -### Task Type Detection -The workflow analyzes the task description for: -- **Model Indicators**: "model", "User", "Post", "association", "validation" -- **Controller Indicators**: "controller", "action", "CRUD", "API", "filter" -- **View Indicators**: "view", "template", "form", "layout", "responsive" -- **Feature Indicators**: "auth", "admin", "search", "email", "upload" - -### Browser Testing Strategy -Based on application type detected: -- **Blog Applications**: Post CRUD, commenting, navigation -- **E-commerce Applications**: Product catalog, shopping cart, checkout -- **Admin Applications**: User management, authentication, dashboards -- **API Applications**: Endpoint testing, JSON responses, authentication - -## Comparison Benefits vs MCP Tool - -### Advantages of Slash Command Approach -- **Flexibility**: Claude Code can adapt the workflow dynamically -- **Error Handling**: Better error recovery and human-readable feedback -- **Documentation Integration**: Direct access to `.ai` folder without MCP resource limitations -- **Comprehensive Testing**: TestBox BDD specs + Browser testing + Integration testing -- **Test Coverage**: Mandatory 90%+ coverage with detailed analysis -- **Quality Assurance**: No feature complete without passing test suite -- **Reporting**: Rich, detailed reporting with screenshots, test results, and coverage analysis -- **Learning**: Users see the complete process and can learn from it - -### Testing Strategy -Run both approaches on the same task: -``` -/wheels_execute create a blog with posts and comments -vs -mcp__wheels__develop(task="create a blog with posts and comments") -``` - -Compare results on: -- Code quality and adherence to patterns -- Test coverage and browser testing thoroughness -- Error prevention and pattern consistency -- Implementation time and reliability -- User experience and learning value - -This slash command provides a systematic, comprehensive approach to Wheels development that ensures professional quality results with complete testing coverage. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index f0ba3cb637..1d505033dc 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1035 +1,220 @@ -# CLAUDE.md +# Wheels Framework -This file provides guidance to Claude Code (claude.ai/code) when working with a Wheels application. +CFML MVC framework with ActiveRecord ORM. Models in `app/models/`, controllers in `app/controllers/`, views in `app/views/`, migrations in `app/migrator/migrations/`, config in `config/`, tests in `tests/`. -## 🚨 MANDATORY: Pre-Implementation Workflow +## Directory Layout -**AI ASSISTANTS MUST FOLLOW THIS EXACT ORDER:** - -### 🛑 STEP 1: CHECK MCP TOOLS AVAILABILITY (ALWAYS FIRST) -```bash -# Check if .mcp.json exists - if YES, MCP tools are MANDATORY -ls .mcp.json ``` - -**If `.mcp.json` exists, YOU MUST:** -- ✅ Use `mcp__wheels__*` tools for ALL development tasks -- ❌ NEVER use CLI commands (`wheels g`, `wheels test`, etc.) -- ❌ NEVER use bash/curl for Wheels operations - -### 🛑 STEP 2: VERIFY MCP TOOLS WORK -```javascript -// Test MCP server connection BEFORE any development -mcp__wheels__wheels_server(action="status") +app/controllers/ app/models/ app/views/ app/views/layout.cfm +app/migrator/migrations/ app/events/ app/global/ app/lib/ +app/mailers/ app/jobs/ app/plugins/ app/snippets/ +config/settings.cfm config/routes.cfm config/environment.cfm +public/ tests/ vendor/ .env (never commit) ``` -### 🛑 STEP 3: Load Documentation -1. **📖 Load Relevant .ai Documentation** - - Check if `.ai/` folder exists in project root - - Load appropriate documentation sections: - - For models: Read `.ai/wheels/database/` and `.ai/cfml/components/` - - For controllers: Read `.ai/wheels/controllers/` and `.ai/cfml/syntax/` - - For CFML syntax: Read `.ai/cfml/syntax/` and `.ai/cfml/best-practices/` - - For patterns: Read `.ai/wheels/patterns/` and `.ai/wheels/snippets/` - -2. **✅ Validate Against Standards** - - Confirm implementation matches patterns in `.ai/wheels/patterns/` - - Verify CFML syntax follows `.ai/cfml/best-practices/` - - Check security practices from `.ai/wheels/security/` - - Ensure naming conventions match `.ai/wheels/core-concepts/` - -3. **🔍 Use Established Code Examples** - - Reference code templates from `.ai/wheels/snippets/` - - Follow model patterns from `.ai/wheels/database/models/` - - Apply controller patterns from `.ai/wheels/controllers/` - -**If `.ai/` folder is not available, use the MCP resources:** -- `wheels://.ai/cfml/syntax` - CFML language fundamentals -- `wheels://.ai/wheels/patterns` - Framework patterns -- `wheels://.ai/wheels/snippets` - Code examples - -## 🎯 Slash Commands (NEW!) - -**The Wheels MCP server now supports slash commands for faster development workflows!** - -### ✅ Available Slash Commands - -Use these slash commands in supported MCP clients: - -- **`/wheels-develop`** - Complete end-to-end development workflow - - Example: `/wheels-develop create a blog with posts and comments` - - Parameters: `task` (required), `verbose` (optional), `skip_browser_test` (optional) - -- **`/wheels-generate`** - Generate Wheels components - - Example: `/wheels-generate model User name:string,email:string` - - Parameters: `type` (required), `name` (required), `attributes` (optional), `actions` (optional) - -- **`/wheels-migrate`** - Run database migrations - - Example: `/wheels-migrate latest` - - Parameters: `action` (required: latest, up, down, reset, info) - -- **`/wheels-test`** - Run tests - - Example: `/wheels-test` - - Parameters: `target` (optional), `verbose` (optional) - -- **`/wheels-server`** - Manage development server - - Example: `/wheels-server status` - - Parameters: `action` (required: start, stop, restart, status) - -- **`/wheels-reload`** - Reload application - - Example: `/wheels-reload` - - Parameters: `password` (optional) - -- **`/wheels-analyze`** - Analyze project structure - - Example: `/wheels-analyze all` - - Parameters: `target` (required: models, controllers, routes, migrations, tests, all), `verbose` (optional) +## Development Tools -### 🚀 Slash Command Benefits +Prefer MCP tools when the Wheels MCP server is available (`mcp__wheels__*`). Fall back to CLI otherwise. -- **Faster workflows** - Single command for complex operations -- **Natural language** - Describe what you want to build -- **Integrated testing** - Automatic validation and browser testing -- **Documentation loading** - Auto-loads relevant .ai docs -- **Error handling** - Intelligent error recovery +| Task | MCP | CLI | +|------|-----|-----| +| Generate | `wheels_generate(type, name, attributes)` | `wheels g model/controller/scaffold Name attrs` | +| Migrate | `wheels_migrate(action="latest\|up\|down\|info")` | `wheels dbmigrate latest\|up\|down\|info` | +| Test | `wheels_test()` | `wheels test run` | +| Reload | `wheels_reload()` | `?reload=true&password=...` | +| Server | `wheels_server(action="status")` | `wheels server start\|stop\|status` | +| Analyze | `wheels_analyze(target="all")` | — | -## Quick Start +## Critical Anti-Patterns (Top 10) -### MCP-Enabled Wheels Development +These are the most common mistakes when generating Wheels code. Check every time. -**🚨 CRITICAL: If `.mcp.json` exists, use MCP tools exclusively** - -### ✅ Common Development Tasks (MCP Tools) -- **Create a model**: `mcp__wheels__wheels_generate(type="model", name="User", attributes="name:string,email:string,active:boolean")` -- **Create a controller**: `mcp__wheels__wheels_generate(type="controller", name="Users", actions="index,show,new,create,edit,update,delete")` -- **Create full scaffold**: `mcp__wheels__wheels_generate(type="scaffold", name="Product", attributes="name:string,price:decimal,instock:boolean")` -- **Run migrations**: `mcp__wheels__wheels_migrate(action="latest")` or `mcp__wheels__wheels_migrate(action="up")` or `mcp__wheels__wheels_migrate(action="down")` -- **Run tests**: `mcp__wheels__wheels_test()` -- **Reload application**: `mcp__wheels__wheels_reload()` -- **Check server status**: `mcp__wheels__wheels_server(action="status")` -- **Analyze project**: `mcp__wheels__wheels_analyze(target="all")` - -### ❌ Legacy CLI Commands (DO NOT USE if .mcp.json exists) -~~- Create a model: `wheels g model User name:string,email:string,active:boolean`~~ -~~- Create a controller: `wheels g controller Users index,show,new,create,edit,update,delete`~~ -~~- Create full scaffold: `wheels g scaffold Product name:string,price:decimal,instock:boolean`~~ -~~- Run migrations: `wheels dbmigrate latest` `wheels dbmigrate up` `wheels dbmigrate down`~~ -~~- Run tests: `wheels test run`~~ -~~- Reload application: Visit `/?reload=true&password=yourpassword`~~ - -**⚠️ Only use CLI commands if:** -1. `.mcp.json` does not exist -2. MCP tools are not available -3. You are setting up a new Wheels project from scratch - -## 🔍 MCP Workflow Validation - -**Before proceeding with ANY development task, AI assistants MUST verify:** - -### ✅ MCP Tools Checklist -1. **Check MCP availability**: `ls .mcp.json` (if exists → MCP is mandatory) -2. **Test MCP connection**: `mcp__wheels__wheels_server(action="status")` -3. **Verify MCP tools list**: `ListMcpResourcesTool(server="wheels")` - -### 🚨 Enforcement Rules -- **If ANY of the following are detected, STOP and use MCP tools instead:** - - Using `wheels g` commands - - Using `wheels dbmigrate` commands - - Using `wheels test` commands - - Using `wheels server` commands - - Using `curl` for Wheels operations - - Using bash commands for Wheels development - -### 🔄 Correct MCP Usage Pattern -```javascript -// 1. Always check server status first -mcp__wheels__wheels_server(action="status") - -// 2. Use MCP tools for all operations -mcp__wheels__wheels_generate(type="model", name="User", attributes="name:string,email:string") -mcp__wheels__wheels_migrate(action="latest") -mcp__wheels__wheels_test() -mcp__wheels__wheels_reload() - -// 3. Analyze results -mcp__wheels__wheels_analyze(target="all") -``` - -## 📚 MCP Tool Usage Examples - -### 🎯 Complete Development Workflow Example -```javascript -// 1. Start every session by checking MCP availability -mcp__wheels__wheels_server(action="status") - -// 2. Create a complete blog system -mcp__wheels__wheels_generate(type="model", name="Post", attributes="title:string,content:text,published:boolean") -mcp__wheels__wheels_generate(type="controller", name="Posts", actions="index,show,new,create,edit,update,delete") -mcp__wheels__wheels_migrate(action="latest") - -// 3. Test and validate -mcp__wheels__wheels_test() -mcp__wheels__wheels_analyze(target="all") - -// 4. Reload when making configuration changes -mcp__wheels__wheels_reload() -``` +### 1. Mixed Argument Styles +Wheels functions cannot mix positional and named arguments. This is the #1 error source. +```cfm +// WRONG — mixed positional + named +hasMany("comments", dependent="delete"); +validatesPresenceOf("name", message="Required"); -### ❌ WRONG: CLI-Based Approach (DO NOT USE) -```bash -# These commands are FORBIDDEN when .mcp.json exists -wheels g model Post title:string,content:text,published:boolean -wheels g controller Posts index,show,new,create,edit,update,delete -wheels dbmigrate latest -wheels test run -curl "http://localhost:8080/?reload=true" -``` +// RIGHT — all named when using options +hasMany(name="comments", dependent="delete"); +validatesPresenceOf(properties="name", message="Required"); -### ✅ CORRECT: MCP-Based Approach (MANDATORY) -```javascript -// Always use MCP tools - they provide better integration and error handling -mcp__wheels__wheels_generate(type="model", name="Post", attributes="title:string,content:text,published:boolean") -mcp__wheels__wheels_generate(type="controller", name="Posts", actions="index,show,new,create,edit,update,delete") -mcp__wheels__wheels_migrate(action="latest") -mcp__wheels__wheels_test() -mcp__wheels__wheels_reload() +// RIGHT — positional only (no options) +hasMany("comments"); +validatesPresenceOf("name"); ``` -### 🔍 Debugging with MCP Tools -```javascript -// Check project status -mcp__wheels__wheels_analyze(target="all", verbose=true) - -// Check migrations -mcp__wheels__wheels_migrate(action="info") - -// Validate models -mcp__wheels__wheels_validate(model="all") +### 2. Query vs Array Confusion in Views +Model finders return query objects, not arrays. Loop accordingly. +```cfm +// WRONG + -// Check server status -mcp__wheels__wheels_server(action="status") +// RIGHT + + #users.firstName# + ``` -## Application Architecture - -### MVC Framework Structure -Wheels follows the Model-View-Controller (MVC) architectural pattern: - -- **Models** (`/app/models/`): Data layer with ActiveRecord ORM, validation, associations -- **Views** (`/app/views/`): Presentation layer with CFML templates, layouts, partials -- **Controllers** (`/app/controllers/`): Request handling, business logic coordination -- **Configuration** (`/config/`): Application settings, routes, environment configurations -- **Database** (`/app/migrator/migrations/`): Version-controlled schema changes -- **Assets** (`/public/`): Static files, CSS, JavaScript, images -- **Tests** (`/tests/`): TestBox unit and integration tests +### 3. No Nested Resource Routes +Wheels does not support Rails-style nested resource blocks. +```cfm +// WRONG +.resources("posts", function(r) { r.resources("comments"); }) -### Directory Structure -``` -/ -├── app/ (Application code) -│ ├── controllers/ (Request handlers) -│ ├── models/ (Data layer) -│ ├── views/ (Templates) -│ ├── migrator/ (Database migrations) -│ ├── events/ (Application events) -│ ├── global/ (Global functions) -│ ├── mailers/ (Email components) -│ ├── jobs/ (Background jobs) -│ ├── lib/ (Custom libraries) -│ ├── plugins/ (Third-party plugins) -│ └── snippets/ (Code templates) -├── config/ (Configuration files) -│ ├── app.cfm (Application.cfc this scope settings) -│ ├── environment.cfm (Current environment) -│ ├── routes.cfm (URL routing) -│ ├── settings.cfm (Framework settings) -│ └── [environment]/ (Environment-specific overrides) -├── public/ (Web-accessible files) -│ ├── files/ (User uploads, sendFile() content) -│ ├── images/ (Image assets) -│ ├── javascripts/ (JavaScript files) -│ ├── stylesheets/ (CSS files) -│ ├── miscellaneous/ (Miscellaneous files) -│ ├── Application.cfc (Framework bootstrap) -│ └── index.cfm (Entry point) -├── tests/ (Test files) -├── vendor/ (Dependencies) -├── .env (Environment variables - NEVER commit) -├── box.json (Package configuration) -└── server.json (CommandBox server configuration) +// RIGHT — separate declarations +.resources("posts") +.resources("comments") ``` -## Development Commands - -### Code Generation -```bash -# Generate MVC components -wheels g model User name:string,email:string,active:boolean -wheels g controller Users index,show,new,create,edit,update,delete -wheels g view users/dashboard - -# Generate full CRUD scaffold -wheels g scaffold Product name:string,price:decimal,instock:boolean - -# Generate database migrations -wheels g migration CreateUsersTable -wheels g migration AddEmailToUsers --attributes="email:string:index" +### 4. Non-Existent Form Helpers +These helpers don't exist in Wheels: `emailField()`, `urlField()`, `numberField()`, `phoneField()`. +```cfm +// WRONG +#emailField(objectName="user", property="email")# -# Generate other components -wheels g mailer UserNotifications --methods="welcome,passwordReset" -wheels g job ProcessOrders --queue=high -wheels g test model User -wheels g helper StringUtils +// RIGHT +#textFieldTag(name="user[email]", type="email", value=user.email)# ``` -### Migration Management -```bash -# Check migration status -wheels dbmigrate info - -# Migration to Latest -wheels dbmigrate latest - -# Migration to version 0 -wheels dbmigrate reset - -# Migration one version UP -wheels dbmigrate up +### 5. Migration Seed Data — Use Direct SQL +Parameter binding in `execute()` is unreliable. Use inline SQL for seed data. +```cfm +// WRONG +execute(sql="INSERT INTO roles (name) VALUES (?)", parameters=[{value="admin"}]); -# Migration one version DOWN -wheels dbmigrate down +// RIGHT +execute("INSERT INTO roles (name, createdAt, updatedAt) VALUES ('admin', NOW(), NOW())"); ``` -### Server Management -```bash -# Start/stop development server -wheels server start -wheels server stop -wheels server restart - -# View server status -wheels server status - -# View server logs -wheels server log --follow +### 6. Route Order Matters +Routes are matched first-to-last. Wrong order = wrong matches. ``` - -### Testing -```bash -# Run all tests -wheels test run +Order: MCP routes → resources → custom named routes → root → wildcard (last!) ``` -## Configuration Management - -### Environment Settings -Set your environment in `/config/environment.cfm`: +### 7. timestamps() Includes createdAt and updatedAt +Don't also add separate datetime columns for these. ```cfm - - set(environment="development"); - -``` - -**Available Environments:** -- `development` - Local development with debug info -- `testing` - Automated testing environment -- `maintenance` - Maintenance mode with limited access -- `production` - Live production environment +// WRONG — duplicates +t.timestamps(); +t.datetime(columnNames="createdAt"); -### Framework Settings -Configure global settings in `/config/settings.cfm`: -```cfm - - // Database configuration - set(dataSourceName="myapp-dev"); - set(dataSourceUserName="username"); - set(dataSourcePassword="password"); - - // URL rewriting - set(URLRewriting="On"); - - // Reload password - set(reloadPassword="mypassword"); - - // Error handling - set(showErrorInformation=true); - set(sendEmailOnError=false); - +// RIGHT +t.timestamps(); // creates both createdAt and updatedAt ``` -### Environment-Specific Overrides -Create environment-specific settings in `/config/[environment]/settings.cfm`: +### 8. Database-Agnostic Dates in Migrations +Use `NOW()` — it works across MySQL, PostgreSQL, SQL Server, H2. ```cfm -// /config/production/settings.cfm - - set(dataSourceName="myapp-prod"); - set(showErrorInformation=false); - set(sendEmailOnError=true); - set(cachePages=true); - -``` +// WRONG — database-specific +execute("INSERT INTO users (name, createdAt) VALUES ('Admin', CURRENT_TIMESTAMP)"); -## URL Routing - -### Default Route Pattern -URLs follow the pattern: `[controller]/[action]/[key]` - -**Examples:** -- `/users` → `Users.cfc`, `index()` action -- `/users/show/12` → `Users.cfc`, `show()` action, `params.key = 12` - -### Custom Routes -Define custom routes in `/config/routes.cfm`: -```cfm - -mapper() - // Named routes - .get(name="login", to="sessions##new") - .post(name="authenticate", to="sessions##create") - - // RESTful resources - .resources("users") - .resources("products", except="destroy") - - // Nested resources - use separate declarations - .resources("users") - .resources("orders") - - // Root route - .root(to="home##index", method="get") - - // Wildcard (keep last) - .wildcard() -.end(); - +// RIGHT +execute("INSERT INTO users (name, createdAt, updatedAt) VALUES ('Admin', NOW(), NOW())"); ``` -### Route Helpers +### 9. Controller Filters Must Be Private +Filter functions (authentication, data loading) must be declared `private`. ```cfm -// Link generation -#linkTo(route="user", key=user.id, text="View User")# -#linkTo(controller="products", action="index", text="All Products")# - -// Form generation -#startFormTag(route="user", method="put", key=user.id)# +// WRONG — public filter becomes a routable action +function authenticate() { ... } -// URL generation -#urlFor(route="users")# - -// Redirects in controllers -redirectTo(route="user", key=user.id); +// RIGHT +private function authenticate() { ... } ``` -## Model-View-Controller Patterns - -### Controller Structure +### 10. Always cfparam View Variables +Every variable passed from controller to view needs a cfparam declaration. ```cfm -component extends="Controller" { - - function config() { - // Filters for authentication/authorization - filters(through="authenticate", except="index"); - filters(through="findUser", only="show,edit,update,delete"); - - // Parameter verification - verifies(except="index,new,create", params="key", paramsTypes="integer"); - - // Content type support - provides("html,json"); - } - - function index() { - users = model("User").findAll(order="createdat DESC"); - } +// At top of every view file + + +``` - function create() { - user = model("User").new(params.user); - - if (user.save()) { - redirectTo(route="user", key=user.id, success="User created!"); - } else { - renderView(action="new"); - } - } +## Wheels Conventions - private function authenticate() { - if (!session.authenticated) { - redirectTo(controller="sessions", action="new"); - } - } +- **config()**: All model associations/validations/callbacks and controller filters/verifies go in `config()` +- **Naming**: Models are singular PascalCase (`User.cfc`), controllers are plural PascalCase (`Users.cfc`), table names are plural lowercase (`users`) +- **Parameters**: `params.key` for URL key, `params.user` for form struct, `params.user.firstName` for nested +- **extends**: Models extend `"Model"`, controllers extend `"Controller"`, tests extend `"wheels.Test"` or `"wheels.Testbox"` +- **Associations**: All named params when using options: `hasMany(name="orders")`, `belongsTo(name="user")`, `hasOne(name="profile")` +- **Validations**: Property param is `property` (singular) for single, `properties` (plural) for list: `validatesPresenceOf(properties="name,email")` - function sendWelcomeEmail() { - sendEmail( - template="users/welcome", - from="noreply@myapp.com", - to=user.email, - subject="Welcome to MyApp!", - user=user - ); - } +## Model Quick Reference - function downloadReport() { - sendFile( - file="report.pdf", - name="Monthly Report.pdf", - type="application/pdf", - disposition="attachment", - directory="/reports/" - ); - } - - function requireSSL() { - if (!isSecure()) { - redirectTo(protocol="https"); - } - } -} -``` - -### Model Structure ```cfm component extends="Model" { - function config() { - // Associations - hasMany("orders"); - belongsTo("role"); - + // Table/key (only if non-conventional) + tableName("tbl_users"); + setPrimaryKey("userId"); + + // Associations — all named params when using options + hasMany(name="orders", dependent="delete"); + belongsTo(name="role"); + // Validations - validatesPresenceOf("firstname,lastname,email"); + validatesPresenceOf("firstName,lastName,email"); validatesUniquenessOf(property="email"); validatesFormatOf(property="email", regEx="^[\w\.-]+@[\w\.-]+\.\w+$"); - - // Callbacks - beforeSave("hashPassword"); - afterCreate("sendWelcomeEmail"); - - // Nested properties for associations - nestedProperties(association="addresses", allowDelete=true, autoSave=true); - - // Custom finder methods (Wheels doesn't have scope() - use custom finder methods instead) - } - - function findByEmail(required string email) { - return findOne(where="email = '#arguments.email#'"); - } - - function findActive() { - return findAll(where="active = 1"); - } - function findFirst() { - return findFirst(property="createdAt"); - } - - function fullName() { - return trim("#firstname# #lastname#"); - } - - function reload() { - // Reload this model instance from the database - return super.reload(); - } -} -``` - -### View Structure -```cfm - - - - - - - - #csrfMetaTags()# - #contentFor("title", "MyApp")# - #styleSheetLinkTag("application")# - - -
    - #flashMessages()# - #includeContent()# -
    - #javaScriptIncludeTag("application")# - - -
    - - - - -#contentFor("title", "Users")# - -

    Users

    -#linkTo(route="newUser", text="New User", class="btn btn-primary")# - - - - - - - - - - -
    #linkTo(route="user", key=users.id, text=users.firstname)##users.email# - #linkTo(route="editUser", key=users.id, text="Edit")# - #buttonTo(route="user", method="delete", key=users.id, - text="Delete", confirm="Are you sure?")# -
    - -

    No users found.

    -
    -
    -``` - -## Database Migrations - -### Migration Workflow -```bash -# Generate new migration -wheels g migration CreateUsersTable - -# Generate migration with attributes -wheels g migration AddEmailToUsers --attributes="email:string:index" - -# Run pending migrations -wheels dbmigrate latest - -# Rollback migrations -wheels dbmigrate down -``` - -### Migration Example -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - t = createTable(name="users", force=false); - t.string(columnNames="firstName,lastName", allowNull=false); - t.string(columnNames="email", limit=100, allowNull=false); - t.boolean(columnNames="active", default=true); - t.timestamps(); - t.create(); - - addIndex(table="users", columnNames="email", unique=true); - } - } - - function down() { - dropTable("users"); + // Callbacks + beforeSave("sanitizeInput"); } } ``` -### Column Types -```cfm -t.string(columnNames="name", limit=255, allowNull=false, default=""); -t.text(columnNames="description", allowNull=true); -t.integer(columnNames="count", allowNull=false, default=0); -t.decimal(columnNames="price", precision=10, scale=2); -t.boolean(columnNames="active", default=false); -t.date(columnNames="eventDate"); -t.datetime(columnNames="createdAt"); // Use for createdAt/updatedAt only when not using timestamps(); OK for other columns -t.timestamps(); // Creates createdAt and updatedAt -t.integer(columnNames="userId", allowNull=false); // Foreign key -``` +Finders: `model("User").findAll()`, `model("User").findOne(where="...")`, `model("User").findByKey(params.key)`. +Create: `model("User").new(params.user)` then `.save()`, or `model("User").create(params.user)`. +Include associations: `findAll(include="role,orders")`. Pagination: `findAll(page=params.page, perPage=25)`. -### Advanced Migration Features +## Routing Quick Reference ```cfm -// Create database views -component extends="wheels.migrator.Migration" { - function up() { - v = createView(name="activeUsers"); - v.sql("SELECT id, name, email FROM users WHERE active = 1"); - v.create(); - } -} - -// Modify existing tables -component extends="wheels.migrator.Migration" { - function up() { - t = changeTable(name="users"); - t.string(columnNames="middleName", limit=100); - t.change(); - - // Add indexes - addIndex(table="users", columnNames="email", unique=true); - addIndex(table="users", columnNames="lastName,firstName"); - - // Rename tables - renameTable(oldName="user_profiles", newName="profiles"); - } - - function down() { - removeIndex(table="users", indexName="users_email"); - removeIndex(table="users", indexName="users_lastName_firstName"); - renameTable(oldName="profiles", newName="user_profiles"); - - t = changeTable(name="users"); - t.removeColumn(columnNames="middleName"); - t.change(); - } -} +// config/routes.cfm +mapper() + .resources("users") // standard CRUD + .resources("products", except="delete") // skip actions + .get(name="login", to="sessions##new") // named route + .post(name="authenticate", to="sessions##create") + .root(to="home##index", method="get") // homepage + .wildcard() // keep last! +.end(); ``` -## Testing +Helpers: `linkTo(route="user", key=user.id, text="View")`, `urlFor(route="users")`, `redirectTo(route="user", key=user.id)`, `startFormTag(route="user", method="put", key=user.id)`. -### Test Structure -``` -tests/ -├── Test.cfc (Base test component) -├── controllers/ (Controller tests) -├── models/ (Model tests) -└── integration/ (Integration tests) -``` +## Testing Quick Reference -### Model Testing ```cfm component extends="wheels.Testbox" { - - function beforeAll() { - // Setup for all tests in this spec - variables.testData = {}; - } - - function afterAll() { - // Cleanup after all tests - } - - function beforeEach() { - // Setup before each test - variables.user = ""; - } - - function afterEach() { - // Cleanup after each test - if (isObject(variables.user)) { - variables.user.delete(); - } - } - function run() { - describe("User Model", function() { - - it("should be invalid when no data provided", function() { + describe("User", function() { + it("validates presence of name", function() { var user = model("User").new(); - expect(user.valid()).toBeFalse("User should be invalid without data"); - expect(arrayLen(user.allErrors())).toBeGT(0, "Should have validation errors"); - }); - - it("should create user with valid data", function() { - var userData = { - firstname = "John", - lastname = "Doe", - email = "john@example.com" - }; - - var user = model("User").create(userData); - - expect(isObject(user)).toBeTrue("Should return user object"); - expect(user.valid()).toBeTrue("User should be valid"); - expect(user.firstname).toBe("John", "Should set firstname correctly"); + expect(user.valid()).toBeFalse(); }); }); } } ``` -## Security Best Practices - -### CSRF Protection -```cfm -// In controllers -function config() { - protectsFromForgery(); // Enable CSRF protection -} - -// In forms -#startFormTag(route="user", method="put", key=user.id)# - #hiddenFieldTag("authenticityToken", authenticityToken())# - -#endFormTag()# - -// In layout head -#csrfMetaTags()# -``` - -### Input Validation -```cfm -// Parameter verification -function config() { - verifies(only="show,edit,update,delete", params="key", paramsTypes="integer"); - verifies(only="create,update", params="user", paramsTypes="struct"); -} - -// Model validation -function config() { - validatesPresenceOf("firstname,lastname,email"); - validatesFormatOf(property="email", regEx="^[\w\.-]+@[\w\.-]+\.\w+$"); - validatesLengthOf(property="password", minimum=8); -} -``` - -### SQL Injection Prevention -```cfm -// Use model methods (automatically sanitized) -users = model("User").findAll(where="email = '#params.email#'"); - -// Or use cfqueryparam in custom queries -sql = "SELECT * FROM users WHERE email = :email"; -users = queryExecute(sql, { email = { value = params.email, cfsqltype = "cf_sql_varchar" } }, {datasource = yourDatasourceName}); -``` - -## Performance Optimization - -### Caching -```cfm -// Page caching -function config() { - caches(action="index", time=30); // Cache for 30 minutes -} - -// Query caching -users = model("User").findAll(cache=60); // Cache for 60 minutes -``` - -### Database Optimization -```cfm -// Use includes to avoid N+1 queries -users = model("User").findAll(include="role,orders"); - -// Use select to limit columns -users = model("User").findAll(select="id,firstname,lastname,email"); - -// Use pagination -users = model("User").findAll(page=params.page, perPage=25); -``` - -## Deployment - -### Production Configuration -```cfm -// /config/production/settings.cfm - - // Database - set(dataSourceName="myapp-prod"); - - // Security - set(showErrorInformation=false); - set(sendEmailOnError=true); - - // Performance - set(cachePages=true); - set(cachePartials=true); - set(cacheQueries=true); - -``` - -### Environment Variables -Use `.env` file for sensitive configuration (never commit to version control): -```bash -# .env -DATABASE_URL=mysql://user:pass@localhost:3306/myapp_prod -SMTP_HOST=smtp.example.com -API_KEY=your-secret-api-key -``` - -Access in configuration: -```cfm - - if (FileExists(ExpandPath("/.env"))) { - set(dataSourceName=GetEnv("DATABASE_NAME")); - set(dataSourceUserName=GetEnv("DATABASE_USER")); - set(dataSourcePassword=GetEnv("DATABASE_PASSWORD")); - } - -``` - -## 🚨 MANDATORY: Native MCP Server - -**This Wheels application includes a native CFML MCP (Model Context Protocol) server that MUST be used by AI assistants for all development tasks.** - -**🔴 CRITICAL RULE: If `.mcp.json` exists, ALL development MUST use MCP tools - no exceptions.** - -The MCP server eliminates the need for Node.js dependencies and provides AI coding assistants with direct, integrated access to your Wheels application. - -### Accessing the MCP Server - -The MCP server is available at `/wheels/mcp` and supports: -- **Resources**: Documentation, guides, project context, patterns -- **Tools**: Code generation (models, controllers, views, migrations) -- **Prompts**: Context-aware help for Wheels development - -### MCP Client Configuration - -Configure your AI coding assistant to use the native MCP server: - -```json -{ - "mcpServers": { - "wheels": { - "type": "http", - "url": "http://localhost:8080/wheels/mcp" - } - } -} -``` - -Replace `8080` with your development server port. - -### Available Tools - -- `wheels_generate` - Generate components (models, controllers, etc.) -- `wheels_migrate` - Run database migrations -- `wheels_test` - Execute tests -- `wheels_server` - Manage development server -- `wheels_reload` - Reload application - -### Route Configuration - -The MCP server routes are pre-configured in `/config/routes.cfm`: - -```cfm -.post(pattern="/wheels/mcp", to="##mcp") -.get(pattern="/wheels/mcp", to="##mcp") -``` - -These routes must come before the `.wildcard()` route to function correctly. - -## Common Patterns - -### Service Layer Pattern -```cfm -// /app/lib/UserService.cfc -component { - - function createUser(required struct userData) { - local.user = model("User").new(arguments.userData); - - transaction { - if (local.user.save()) { - sendWelcomeEmail(local.user); - return local.user; - } else { - transaction action="rollback"; - return false; - } - } - } -} -``` - -### API Development -```cfm -// API base controller -component extends="wheels.Controller" { - - function config() { - provides("json"); - filters(through="authenticate"); - } - - private function authenticate() { - // API authentication logic - } -} - -// API endpoint -function index() { - users = model("User").findAll(); - renderWith(data={users=users}); -} -``` - -### Error Handling -```cfm -// Global error handler in Application.cfc -function onError(exception, eventname) { - if (get("environment") == "production") { - WriteLog(file="application", text=exception.message, type="error"); - include "/app/views/errors/500.cfm"; - } else { - return true; // Let ColdFusion handle it - } -} -``` - -## Common Issues and Troubleshooting +Tests live in `tests/models/`, `tests/controllers/`, `tests/integration/`. Run with MCP `wheels_test()` or CLI `wheels test run`. -### Association Errors -**"Missing argument name" in hasMany()** -This error occurs when mixing positional and named parameters in Wheels function calls: +## Reference Docs -❌ **Incorrect (mixed parameter styles):** -```cfm -hasMany("comments", dependent="delete"); // Error: can't mix positional and named -``` +Deeper documentation lives in `.ai/` — Claude will search it automatically when needed: +- `.ai/cfml/` — CFML language reference (syntax, data types, components) +- `.ai/wheels/models/` — ORM details, associations, validations +- `.ai/wheels/controllers/` — filters, rendering, security +- `.ai/wheels/views/` — layouts, partials, form helpers, link helpers +- `.ai/wheels/database/` — migration column types, queries, advanced operations +- `.ai/wheels/configuration/` — routing, environments, settings -✅ **Correct (consistent named parameters):** -```cfm -hasMany(name="comments", dependent="delete"); -``` - -✅ **Also correct (all positional):** -```cfm -hasMany("comments"); -``` +## MCP Server -Wheels requires consistent parameter syntax - either all positional or all named parameters. - -### Routing Issues -**Incorrect .resources() syntax** -Wheels resource routing syntax differs from Rails: - -❌ **Incorrect (Rails-style nested):** -```cfm -.resources("posts", function(nested) { - nested.resources("comments"); -}) -``` - -✅ **Correct (separate declarations):** -```cfm -.resources("posts") -.resources("comments") -``` - -**Route ordering matters:** resources → custom routes → root → wildcard - -### Form Helper Limitations -Wheels has more limited form helpers compared to Rails: - -❌ **Not available:** -```cfm -#emailField()# // Doesn't exist -#label(text="Name")# // text parameter not supported -``` - -✅ **Use instead:** -```cfm -#textField(type="email")# - -``` - -### Migration Data Seeding -Parameter binding in migrations can be unreliable. Use direct SQL: - -❌ **Problematic:** -```cfm -execute(sql="INSERT INTO posts (title) VALUES (?)", parameters=[{value=title}]); -``` - -✅ **Reliable:** -```cfm -execute("INSERT INTO posts (title, createdAt, updatedAt) VALUES ('My Post', NOW(), NOW())"); -``` +Endpoint: `/wheels/mcp` (routes must come before `.wildcard()` in routes.cfm). -### Debugging Tips -1. Check Wheels documentation - don't assume Rails conventions work -2. Use simple patterns first, add complexity incrementally -3. Test associations and routes in isolation -4. Use `?reload=true` after configuration changes -5. Check debug footer for route information \ No newline at end of file +Tools: `wheels_generate`, `wheels_migrate`, `wheels_test`, `wheels_server`, `wheels_reload`, `wheels_analyze`, `wheels_validate`. diff --git a/examples/tweet/.ai/CLAUDE.md b/examples/tweet/.ai/CLAUDE.md deleted file mode 100755 index c1a332792d..0000000000 --- a/examples/tweet/.ai/CLAUDE.md +++ /dev/null @@ -1,179 +0,0 @@ -# Wheels Documentation Index - -🚨 **COMPREHENSIVE DOCUMENTATION INDEX** 🚨 - -This file provides the complete index of Wheels documentation for AI assistants. All technical content has been organized into the structured `.ai` folder for maximum efficiency and accuracy. - -⛔ **CRITICAL: ALWAYS READ RELEVANT DOCUMENTATION BEFORE WRITING CODE** ⛔ - -## 🚨 MANDATORY Pre-Implementation Workflow - -### 🛑 STEP 1: Critical Error Prevention (ALWAYS FIRST) -1. **`.ai/wheels/troubleshooting/common-errors.md`** - PREVENT FATAL ERRORS -2. **`.ai/wheels/patterns/validation-templates.md`** - VALIDATION CHECKLISTS - -### 📋 STEP 2: Task-Specific Documentation Loading - -#### 🏗️ For Model Development -**MANDATORY Reading Order:** -1. `.ai/wheels/models/data-handling.md` - Critical query vs array patterns -2. `.ai/wheels/models/architecture.md` - Model fundamentals and structure -3. `.ai/wheels/models/associations.md` - Relationship patterns (CRITICAL) -4. `.ai/wheels/models/validations.md` - Validation methods and patterns -5. `.ai/wheels/models/best-practices.md` - Model development guidelines - -#### 🎮 For Controller Development -**MANDATORY Reading Order:** -1. `.ai/wheels/controllers/architecture.md` - Controller fundamentals and CRUD -2. `.ai/wheels/controllers/rendering.md` - View rendering and responses -3. `.ai/wheels/controllers/filters.md` - Authentication and authorization -4. `.ai/wheels/controllers/model-interactions.md` - Controller-model patterns -5. `.ai/wheels/controllers/best-practices.md` - Controller development guidelines - -#### 📄 For View Development -**MANDATORY Reading Order:** -1. `.ai/wheels/views/data-handling.md` - CRITICAL query vs array patterns -2. `.ai/wheels/views/architecture.md` - View structure and conventions -3. `.ai/wheels/views/forms.md` - Form helpers and limitations (CRITICAL) -4. `.ai/wheels/views/layouts.md` - Layout patterns and inheritance -5. `.ai/wheels/views/best-practices.md` - View implementation checklist - -#### ⚙️ For Configuration Work -**MANDATORY Reading Order:** -1. `.ai/wheels/configuration/routing.md` - CRITICAL routing anti-patterns -2. `.ai/wheels/configuration/environments.md` - Environment settings -3. `.ai/wheels/configuration/framework-settings.md` - Global settings -4. `.ai/wheels/configuration/best-practices.md` - Configuration guidelines - -### 🔍 STEP 3: Anti-Pattern Validation (BEFORE WRITING CODE) -- [ ] ❌ **NO** mixed argument styles in Wheels functions -- [ ] ❌ **NO** ArrayLen() usage on model associations (use .recordCount) -- [ ] ❌ **NO** Rails-style nested resource routing -- [ ] ❌ **NO** emailField() or passwordField() helpers (don't exist) -- [ ] ✅ **YES** consistent arguments: ALL named OR ALL positional -- [ ] ✅ **YES** use .recordCount: `user.posts().recordCount` -- [ ] ✅ **YES** separate resource declarations -- [ ] ✅ **YES** textField() with type attribute - -## 📚 Complete Documentation Structure - -### Core Framework Components - -#### Models Documentation (`.ai/wheels/models/`) -- `architecture.md` - Model structure and fundamentals -- `data-handling.md` - Critical query vs array patterns -- `associations.md` - Relationship patterns (CRITICAL) -- `validations.md` - Validation rules and methods -- `callbacks.md` - Lifecycle hooks and events -- `methods-reference.md` - Complete method documentation -- `advanced-patterns.md` - Complex model examples -- `user-authentication.md` - Authentication model patterns -- `testing.md` - Model testing strategies -- `performance.md` - Query optimization -- `best-practices.md` - Development guidelines -- `advanced-features.md` - Timestamps and dirty tracking - -#### Controllers Documentation (`.ai/wheels/controllers/`) -- `architecture.md` - Controller structure and CRUD patterns -- `rendering.md` - View rendering, redirects, flash messages -- `filters.md` - Authentication, authorization, data loading -- `model-interactions.md` - Controller-model patterns, validation -- `api.md` - JSON/XML APIs, authentication, versioning -- `security.md` - CSRF, parameter verification, sanitization -- `testing.md` - Controller testing patterns and helpers - -#### Views Documentation (`.ai/wheels/views/`) -- `data-handling.md` - Critical query vs array patterns -- `architecture.md` - View structure and file organization -- `layouts.md` - Layout patterns and inheritance -- `partials.md` - Partial usage and patterns -- `forms.md` - Form helpers and Wheels limitations -- `helpers.md` - View helpers and custom helpers -- `advanced-patterns.md` - AJAX, performance, caching -- `testing.md` - View testing patterns -- `best-practices.md` - Implementation checklist and patterns - -#### Configuration Documentation (`.ai/wheels/configuration/`) -- `routing.md` - CRITICAL routing anti-patterns and patterns -- `environments.md` - Environment settings and switching -- `application.md` - Application.cfc settings (app.cfm) -- `framework-settings.md` - Global framework settings (settings.cfm) -- `overview.md` - File structure, loading order, general overview -- `best-practices.md` - Configuration best practices and patterns -- `troubleshooting.md` - Common issues and debugging -- `security.md` - Security considerations and hardening - -## 🚨 Critical Anti-Pattern Prevention - -### Most Common Wheels Errors -1. **Mixed Arguments**: `hasMany("comments", dependent="delete")` ❌ -2. **Query vs Array Confusion**: `ArrayLen(posts)` on query objects ❌ -3. **Rails-style Routing**: Nested resource functions ❌ -4. **Non-existent Helpers**: `emailField()`, `passwordField()` ❌ - -### Correct Patterns -1. **Consistent Arguments**: `hasMany(name="comments", dependent="delete")` ✅ -2. **Query Methods**: `posts.recordCount` ✅ -3. **Separate Resources**: `.resources("posts").resources("comments")` ✅ -4. **Wheels Helpers**: `textField(type="email")` ✅ - -## 🛠️ AI Assistant Implementation Guidelines - -### 🛑 MANDATORY Pre-Code Actions (NO EXCEPTIONS) -1. **ALWAYS** read `.ai/wheels/troubleshooting/common-errors.md` FIRST -2. **ALWAYS** read component-specific .ai documentation -3. **VALIDATE** against anti-patterns before writing any code -4. **REFERENCE** code examples from .ai documentation as templates -5. **CHECK** implementation against validation templates continuously - -### Quality Assurance Process -1. **Documentation First**: Always consult .ai documentation before coding -2. **Pattern Consistency**: Follow established patterns from .ai documentation -3. **Security Awareness**: Apply security practices from .ai documentation -4. **Convention Adherence**: Follow Wheels naming and structure conventions -5. **Validation**: Test implementations against documented standards - -## 🚀 Quick Reference Dispatchers - -### Component Quick Access -- **Models**: `app/models/CLAUDE.md` → `.ai/wheels/models/` -- **Controllers**: `app/controllers/CLAUDE.md` → `.ai/wheels/controllers/` -- **Views**: `app/views/CLAUDE.md` → `.ai/wheels/views/` -- **Configuration**: Root `CLAUDE.md` → `.ai/wheels/configuration/` - -### Critical Reading Priority -1. **Error Prevention**: `.ai/wheels/troubleshooting/common-errors.md` -2. **Data Handling**: Component-specific `data-handling.md` files -3. **Best Practices**: Component-specific `best-practices.md` files -4. **Architecture**: Component-specific `architecture.md` files - -## ✅ Post-Implementation Validation - -### MANDATORY Validation Commands -```bash -# 1. Syntax validation -wheels server start --validate - -# 2. Test validation -wheels test run - -# 3. Manual anti-pattern check -# Check implementation against .ai documentation patterns -``` - -### If Validation Fails -1. Consult `.ai/wheels/troubleshooting/common-errors.md` -2. Review appropriate component documentation in `.ai/wheels/` -3. Fix errors following documented patterns -4. Re-run validation until all checks pass - -## 🎯 Success Criteria - -**Your implementation is successful when:** -- [ ] All relevant .ai documentation has been read -- [ ] No anti-patterns are present in the code -- [ ] Patterns match those documented in .ai folder -- [ ] Validation commands pass successfully -- [ ] Code follows Wheels conventions and best practices - -🚨 **REMEMBER: The .ai folder contains the definitive, comprehensive documentation. ALWAYS use it as your primary reference!** \ No newline at end of file diff --git a/examples/tweet/CLAUDE.md b/examples/tweet/CLAUDE.md deleted file mode 100755 index e8f1cfdb55..0000000000 --- a/examples/tweet/CLAUDE.md +++ /dev/null @@ -1,2074 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with a Wheels application. - -## 🚨 MANDATORY: Pre-Implementation Workflow - -**AI ASSISTANTS MUST FOLLOW THIS EXACT ORDER:** - -### 🛑 STEP 1: CHECK MCP TOOLS AVAILABILITY (ALWAYS FIRST) -```bash -# Check if .mcp.json exists - if YES, MCP tools are MANDATORY -ls .mcp.json -``` - -**If `.mcp.json` exists, YOU MUST:** -- ✅ Use `mcp__wheels__*` tools for ALL development tasks -- ❌ NEVER use CLI commands (`wheels g`, `wheels test`, etc.) -- ❌ NEVER use bash/curl for Wheels operations - -### 🛑 STEP 2: VERIFY MCP TOOLS WORK -```javascript -// Test MCP server connection BEFORE any development -mcp__wheels__wheels_server(action="status") -``` - -### 🛑 STEP 3: Use Claude Code Skills (MANDATORY) - -**🔴 CRITICAL: Before generating ANY Wheels code, you MUST invoke the appropriate Claude Code skill.** - -Claude Code provides specialized skills that contain deep expertise about Wheels framework patterns and prevent common errors. These skills MUST be used for code generation tasks. - -#### Available Claude Code Skills - -1. **wheels-model-generator** - Generate Wheels ORM models - - Use when: Creating or modifying models, adding validations, defining associations - - Prevents: Mixed argument styles, invalid associations, CFML syntax errors - - Invoke before: Any model creation or modification - -2. **wheels-controller-generator** - Generate Wheels MVC controllers - - Use when: Creating controllers, adding actions, implementing filters - - Prevents: Mixed parameter styles, incorrect rendering, invalid filters - - Invoke before: Any controller creation or modification - -3. **wheels-view-generator** - Generate Wheels view templates - - Use when: Creating views, forms, layouts, or partials - - Prevents: Query/array confusion, incorrect form helpers, association display errors - - Invoke before: Any view creation or modification - -4. **wheels-migration-generator** - Generate database migrations - - Use when: Creating tables, altering schemas, managing database changes - - Prevents: Database-specific SQL, cross-database incompatibility - - Invoke before: Any migration creation or modification - -5. **wheels-test-generator** - Generate TestBox BDD test specs - - Use when: Creating tests for models, controllers, or integration workflows - - Ensures: Comprehensive test coverage with proper Wheels testing conventions - - Invoke before: Any test creation - -6. **wheels-auth-generator** - Generate authentication system - - Use when: Implementing user authentication, login/logout, session management - - Provides: Secure authentication patterns with bcrypt support - - Invoke before: Any authentication implementation - -7. **wheels-api-generator** - Generate RESTful API controllers - - Use when: Creating API endpoints, JSON APIs, or web services - - Ensures: Proper REST conventions and error handling - - Invoke before: Any API controller creation - -8. **wheels-anti-pattern-detector** - Detect and prevent common errors - - Use when: Before generating ANY Wheels code (automatically activated) - - Prevents: Mixed arguments, query confusion, non-existent helpers, database-specific SQL - - ALWAYS active during code generation - -9. **wheels-debugging** - Troubleshoot Wheels errors - - Use when: Encountering errors, exceptions, or unexpected behavior - - Provides: Error analysis, common solutions, debugging strategies - - Invoke when: Debugging issues - -10. **wheels-refactoring** - Refactor Wheels code - - Use when: Optimizing code, fixing anti-patterns, improving performance - - Provides: Refactoring patterns and best practices - - Invoke when: Improving existing code - -11. **wheels-deployment** - Configure production deployment - - Use when: Preparing for production, configuring servers, hardening security - - Provides: Security hardening, performance optimization, environment setup - - Invoke when: Deploying to production - -12. **wheels-documentation-generator** - Generate documentation - - Use when: Documenting code, creating READMEs, generating API docs - - Provides: Documentation comments, README files, API documentation - - Invoke when: Documenting the application - -#### How to Use Skills - -**🚨 MANDATORY WORKFLOW:** - -1. **Identify the task type** (model, controller, view, migration, etc.) -2. **Invoke the appropriate skill FIRST** before any code generation -3. **Follow the skill's guidance** for proper Wheels patterns -4. **Generate code** using MCP tools after skill validation - -**Example Workflows:** - -```javascript -// Creating a model - MUST invoke skill first -Skill("wheels-model-generator") -// Wait for skill to load and provide guidance -// Then use MCP tool: -mcp__wheels__wheels_generate(type="model", name="User", attributes="name:string,email:string") - -// Creating a controller - MUST invoke skill first -Skill("wheels-controller-generator") -// Wait for skill to load and provide guidance -// Then use MCP tool: -mcp__wheels__wheels_generate(type="controller", name="Users", actions="index,show,new,create") - -// Creating a migration - MUST invoke skill first -Skill("wheels-migration-generator") -// Wait for skill to load and provide guidance -// Then use MCP tool: -mcp__wheels__wheels_generate(type="migration", name="CreateUsersTable") -``` - -#### Skill Invocation Rules - -**✅ ALWAYS invoke skills:** -- Before generating ANY Wheels component (model, controller, view, migration) -- When encountering Wheels-specific errors -- When refactoring Wheels code -- When implementing authentication or APIs -- Before deploying to production - -**❌ NEVER skip skills:** -- Skills prevent common Wheels errors and anti-patterns -- Skills ensure proper CFML syntax and Wheels conventions -- Skills provide framework-specific expertise not available in general AI knowledge - -### 🛑 STEP 4: Load Documentation (If Needed) - -**After invoking the appropriate skill**, you may load additional documentation: - -1. **📖 Load Relevant .ai Documentation** - - Check if `.ai/` folder exists in project root - - Load appropriate documentation sections: - - For models: Read `.ai/wheels/database/` and `.ai/cfml/components/` - - For controllers: Read `.ai/wheels/controllers/` and `.ai/cfml/syntax/` - - For CFML syntax: Read `.ai/cfml/syntax/` and `.ai/cfml/best-practices/` - - For patterns: Read `.ai/wheels/patterns/` and `.ai/wheels/snippets/` - -2. **✅ Validate Against Standards** - - Confirm implementation matches patterns in `.ai/wheels/patterns/` - - Verify CFML syntax follows `.ai/cfml/best-practices/` - - Check security practices from `.ai/wheels/security/` - - Ensure naming conventions match `.ai/wheels/core-concepts/` - -3. **🔍 Use Established Code Examples** - - Reference code templates from `.ai/wheels/snippets/` - - Follow model patterns from `.ai/wheels/database/models/` - - Apply controller patterns from `.ai/wheels/controllers/` - -**If `.ai/` folder is not available, use the MCP resources:** -- `wheels://.ai/cfml/syntax` - CFML language fundamentals -- `wheels://.ai/wheels/patterns` - Framework patterns -- `wheels://.ai/wheels/snippets` - Code examples - -## 🚨 MANDATORY: Browser Testing Workflow - -**🔴 CRITICAL: ALL development tasks MUST include comprehensive browser testing - NO EXCEPTIONS** - -### 🛑 STEP 5: MANDATORY BROWSER TESTING (ALWAYS REQUIRED) - -**After ANY development work (models, views, controllers, routes), you MUST:** - -**🔧 Browser Automation Detection** -This project supports both Playwright and Puppeteer MCP tools. You MUST: -1. Check which browser automation tool is available -2. Use the appropriate tool syntax for your testing - -**Detection Logic:** -- If `mcp__playwright__*` tools are available → Use Playwright syntax -- If `mcp__puppeteer__*` tools are available → Use Puppeteer syntax -- Prefer Playwright if both are available (more modern, better features) - ---- - -#### Using Playwright (Preferred) - -1. **📋 Verify Server Status** - ```javascript - mcp__wheels__wheels_server(action="status") - ``` - -2. **🌐 Navigate to Application** - ```javascript - mcp__playwright__browser_navigate(url="http://localhost:[PORT]") - ``` - -3. **📸 Take Screenshot** - ```javascript - mcp__playwright__browser_snapshot() // Accessibility snapshot (better) - // OR - mcp__playwright__browser_take_screenshot(filename="homepage_test.png") - ``` - -4. **🧪 Test Core User Flows** - ```javascript - // Click elements - mcp__playwright__browser_click(element="first post link", ref="[ref-from-snapshot]") - - // Fill forms - mcp__playwright__browser_fill_form(fields=[ - {name: "username", type: "textbox", ref: "[ref]", value: "testuser"}, - {name: "email", type: "textbox", ref: "[ref]", value: "test@example.com"} - ]) - - // Take screenshots after actions - mcp__playwright__browser_take_screenshot(filename="after_action.png") - ``` - -5. **🔍 Verify Interactive Elements** - ```javascript - // Test navigation - mcp__playwright__browser_click(element="navigation link", ref="[ref]") - mcp__playwright__browser_snapshot() - - // Test forms - mcp__playwright__browser_type(element="comment field", ref="[ref]", text="Test comment") - mcp__playwright__browser_click(element="submit button", ref="[ref]") - ``` - ---- - -#### Using Puppeteer (Alternative) - -1. **📋 Verify Server Status** - ```javascript - mcp__wheels__wheels_server(action="status") - ``` - -2. **🌐 Navigate to Application** - ```javascript - mcp__puppeteer__puppeteer_navigate(url="http://localhost:[PORT]") - ``` - -3. **📸 Take Homepage Screenshot** - ```javascript - mcp__puppeteer__puppeteer_screenshot(name="homepage_test", width=1200, height=800) - ``` - -4. **🧪 Test Core User Flows** - ```javascript - // Click elements - mcp__puppeteer__puppeteer_click(selector="article:first-child h2 a") - mcp__puppeteer__puppeteer_screenshot(name="post_detail", width=1200, height=800) - - // Fill forms - mcp__puppeteer__puppeteer_fill(selector="input[name='username']", value="testuser") - - // Test interactive elements - mcp__puppeteer__puppeteer_click(selector="button.btn-primary") - mcp__puppeteer__puppeteer_screenshot(name="interaction_test", width=1200, height=800) - ``` - ---- - -#### Common Testing Requirements (Both Tools) - -**4. 🧪 Test Core User Flows (MANDATORY)** - - **Navigation Testing**: Click all main navigation links - - **CRUD Operations**: Test create, read, update, delete flows - - **Form Interactions**: Test all forms and validation - - **Interactive Elements**: Test JavaScript/Alpine.js/HTMX functionality - - **Responsive Design**: Test on different viewport sizes - -**5. 📊 Document Test Results** - - Confirm all screenshots show expected UI - - Verify no JavaScript errors in console - - Document any issues found - - Ensure responsive design works - -### ❌ DEVELOPMENT IS NOT COMPLETE WITHOUT BROWSER TESTING - -**If you skip browser testing, the implementation is INCOMPLETE and UNACCEPTABLE.** - -**Browser testing must verify:** -- [ ] All pages load correctly -- [ ] Navigation works -- [ ] Forms submit properly -- [ ] Interactive elements (Alpine.js/HTMX) function -- [ ] Responsive design displays correctly -- [ ] No JavaScript errors in console -- [ ] All CRUD operations work end-to-end - -### 🚀 Browser Testing Templates - -#### Playwright Templates (Preferred) - -**For Blog Applications:** -```javascript -// Test homepage -mcp__playwright__browser_navigate(url="http://localhost:PORT") -mcp__playwright__browser_snapshot() // Take accessibility snapshot -mcp__playwright__browser_take_screenshot(filename="blog_homepage.png") - -// Test post detail - first get snapshot to find element refs -mcp__playwright__browser_snapshot() -mcp__playwright__browser_click(element="first post link", ref="[ref-from-snapshot]") -mcp__playwright__browser_take_screenshot(filename="post_detail.png") - -// Test comment form interaction -mcp__playwright__browser_click(element="Add Comment button", ref="[ref]") -mcp__playwright__browser_type(element="comment field", ref="[ref]", text="Test comment") -mcp__playwright__browser_take_screenshot(filename="comment_form.png") - -// Test create post -mcp__playwright__browser_click(element="Write Post link", ref="[ref]") -mcp__playwright__browser_fill_form(fields=[ - {name: "title", type: "textbox", ref: "[ref]", value: "Test Post"}, - {name: "content", type: "textbox", ref: "[ref]", value: "Test content"} -]) -mcp__playwright__browser_take_screenshot(filename="create_post.png") -``` - -**For Admin Applications:** -```javascript -// Test admin dashboard -mcp__playwright__browser_navigate(url="http://localhost:PORT/admin") -mcp__playwright__browser_snapshot() -mcp__playwright__browser_take_screenshot(filename="admin_dashboard.png") - -// Test admin CRUD operations -// ... specific admin testing flows using snapshot + click pattern -``` - -**For API Applications:** -```javascript -// Test API endpoints -mcp__playwright__browser_navigate(url="http://localhost:PORT/api/endpoint") -mcp__playwright__browser_take_screenshot(filename="api_response.png") -``` - ---- - -#### Puppeteer Templates (Alternative) - -**For Blog Applications:** -```javascript -// Test homepage -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT") -mcp__puppeteer__puppeteer_screenshot(name="blog_homepage") - -// Test post detail -mcp__puppeteer__puppeteer_click(selector="article:first-child h2 a") -mcp__puppeteer__puppeteer_screenshot(name="post_detail") - -// Test comment form interaction -mcp__puppeteer__puppeteer_click(selector="button:contains('Add Comment')") -mcp__puppeteer__puppeteer_screenshot(name="comment_form") - -// Test create post -mcp__puppeteer__puppeteer_click(selector="a:contains('Write Post')") -mcp__puppeteer__puppeteer_screenshot(name="create_post") -``` - -**For Admin Applications:** -```javascript -// Test admin dashboard -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/admin") -mcp__puppeteer__puppeteer_screenshot(name="admin_dashboard") - -// Test admin CRUD operations -// ... specific admin testing flows -``` - -**For API Applications:** -```javascript -// Test API endpoints -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT/api/endpoint") -mcp__puppeteer__puppeteer_screenshot(name="api_response") -``` - -## Quick Start - -### Complete Development Workflow - -**🚨 MANDATORY ORDER OF OPERATIONS:** - -1. **Check for MCP tools** (`ls .mcp.json`) -2. **Verify MCP server** (`mcp__wheels__wheels_server(action="status")`) -3. **Invoke appropriate Claude Code skill** (e.g., `Skill("wheels-model-generator")`) -4. **Generate code using MCP tools** (e.g., `mcp__wheels__wheels_generate(...)`) -5. **Test in browser** (using Playwright or Puppeteer MCP tools - see browser testing section) - -### MCP-Enabled Wheels Development - -**🚨 CRITICAL: If `.mcp.json` exists, use MCP tools exclusively** - -### ✅ Common Development Tasks (Skills + MCP Tools) - -**IMPORTANT: Always invoke the appropriate skill BEFORE using MCP tools** - -- **Create a model**: - 1. `Skill("wheels-model-generator")` (FIRST) - 2. `mcp__wheels__wheels_generate(type="model", name="User", attributes="name:string,email:string,active:boolean")` - -- **Create a controller**: - 1. `Skill("wheels-controller-generator")` (FIRST) - 2. `mcp__wheels__wheels_generate(type="controller", name="Users", actions="index,show,new,create,edit,update,delete")` - -- **Create a view**: - 1. `Skill("wheels-view-generator")` (FIRST) - 2. `mcp__wheels__wheels_generate(type="view", name="users/index")` - -- **Create a migration**: - 1. `Skill("wheels-migration-generator")` (FIRST) - 2. `mcp__wheels__wheels_generate(type="migration", name="CreateUsersTable")` - -- **Create tests**: - 1. `Skill("wheels-test-generator")` (FIRST) - 2. `mcp__wheels__wheels_generate(type="test", name="User")` - -- **Create authentication**: - 1. `Skill("wheels-auth-generator")` (FIRST) - 2. Follow skill guidance for auth implementation - -- **Create API**: - 1. `Skill("wheels-api-generator")` (FIRST) - 2. Follow skill guidance for API implementation - -- **Run migrations**: `mcp__wheels__wheels_migrate(action="latest")` -- **Run tests**: `mcp__wheels__wheels_test()` -- **Reload application**: `mcp__wheels__wheels_reload()` -- **Check server status**: `mcp__wheels__wheels_server(action="status")` -- **Analyze project**: `mcp__wheels__wheels_analyze(target="all")` -- **Debug errors**: `Skill("wheels-debugging")` -- **Refactor code**: `Skill("wheels-refactoring")` - -### ❌ Legacy CLI Commands (DO NOT USE if .mcp.json exists) -~~- Create a model: `wheels g model User name:string,email:string,active:boolean`~~ -~~- Create a controller: `wheels g controller Users index,show,new,create,edit,update,delete`~~ -~~- Create full scaffold: `wheels g scaffold Product name:string,price:decimal,instock:boolean`~~ -~~- Run migrations: `wheels dbmigrate latest` `wheels dbmigrate up` `wheels dbmigrate down`~~ -~~- Run tests: `wheels test run`~~ -~~- Reload application: Visit `/?reload=true&password=yourpassword`~~ - -**⚠️ Only use CLI commands if:** -1. `.mcp.json` does not exist -2. MCP tools are not available -3. You are setting up a new Wheels project from scratch - -## 🔍 MCP Workflow Validation - -**Before proceeding with ANY development task, AI assistants MUST verify:** - -### ✅ MCP Tools Checklist -1. **Check MCP availability**: `ls .mcp.json` (if exists → MCP is mandatory) -2. **Test MCP connection**: `mcp__wheels__wheels_server(action="status")` -3. **Verify MCP tools list**: `ListMcpResourcesTool(server="wheels")` - -### 🚨 Enforcement Rules -- **If ANY of the following are detected, STOP and use MCP tools instead:** - - Using `wheels g` commands - - Using `wheels dbmigrate` commands - - Using `wheels test` commands - - Using `wheels server` commands - - Using `curl` for Wheels operations - - Using bash commands for Wheels development - -### 🔄 Correct MCP Usage Pattern -```javascript -// 1. Always check server status first -mcp__wheels__wheels_server(action="status") - -// 2. Use MCP tools for all operations -mcp__wheels__wheels_generate(type="model", name="User", attributes="name:string,email:string") -mcp__wheels__wheels_migrate(action="latest") -mcp__wheels__wheels_test() -mcp__wheels__wheels_reload() - -// 3. Analyze results -mcp__wheels__wheels_analyze(target="all") -``` - -## 📚 MCP Tool Usage Examples - -### 🎯 Complete Development Workflow Example -```javascript -// 1. Start every session by checking MCP availability -mcp__wheels__wheels_server(action="status") - -// 2. Create a complete blog system - INVOKE SKILLS FIRST -Skill("wheels-model-generator") -// Wait for skill to load, then: -mcp__wheels__wheels_generate(type="model", name="Post", attributes="title:string,content:text,published:boolean") - -Skill("wheels-controller-generator") -// Wait for skill to load, then: -mcp__wheels__wheels_generate(type="controller", name="Posts", actions="index,show,new,create,edit,update,delete") - -Skill("wheels-view-generator") -// Wait for skill to load, then: -mcp__wheels__wheels_generate(type="view", name="posts/index") - -Skill("wheels-migration-generator") -// Wait for skill to load, then: -mcp__wheels__wheels_migrate(action="latest") - -// 3. Test and validate -Skill("wheels-test-generator") -// Wait for skill to load, then: -mcp__wheels__wheels_test() -mcp__wheels__wheels_analyze(target="all") - -// 4. Browser testing (MANDATORY) - Use Playwright OR Puppeteer -// Playwright (preferred): -mcp__playwright__browser_navigate(url="http://localhost:8080") -mcp__playwright__browser_snapshot() -mcp__playwright__browser_take_screenshot(filename="homepage.png") - -// OR Puppeteer (alternative): -mcp__puppeteer__puppeteer_navigate(url="http://localhost:8080") -mcp__puppeteer__puppeteer_screenshot(name="homepage") - -// 5. Reload when making configuration changes -mcp__wheels__wheels_reload() -``` - -### ❌ WRONG: CLI-Based Approach (DO NOT USE) -```bash -# These commands are FORBIDDEN when .mcp.json exists -wheels g model Post title:string,content:text,published:boolean -wheels g controller Posts index,show,new,create,edit,update,delete -wheels dbmigrate latest -wheels test run -curl "http://localhost:8080/?reload=true" -``` - -### ✅ CORRECT: MCP-Based Approach (MANDATORY) -```javascript -// Always use MCP tools - they provide better integration and error handling -mcp__wheels__wheels_generate(type="model", name="Post", attributes="title:string,content:text,published:boolean") -mcp__wheels__wheels_generate(type="controller", name="Posts", actions="index,show,new,create,edit,update,delete") -mcp__wheels__wheels_migrate(action="latest") -mcp__wheels__wheels_test() -mcp__wheels__wheels_reload() -``` - -### 🔍 Debugging with MCP Tools -```javascript -// Check project status -mcp__wheels__wheels_analyze(target="all", verbose=true) - -// Check migrations -mcp__wheels__wheels_migrate(action="info") - -// Validate models -mcp__wheels__wheels_validate(model="all") - -// Check server status -mcp__wheels__wheels_server(action="status") -``` - -## Application Architecture - -### MVC Framework Structure -Wheels follows the Model-View-Controller (MVC) architectural pattern: - -- **Models** (`/app/models/`): Data layer with ActiveRecord ORM, validation, associations -- **Views** (`/app/views/`): Presentation layer with CFML templates, layouts, partials -- **Controllers** (`/app/controllers/`): Request handling, business logic coordination -- **Configuration** (`/config/`): Application settings, routes, environment configurations -- **Database** (`/app/migrator/migrations/`): Version-controlled schema changes -- **Assets** (`/public/`): Static files, CSS, JavaScript, images -- **Tests** (`/tests/`): TestBox unit and integration tests - -### Directory Structure -``` -/ -├── app/ (Application code) -│ ├── controllers/ (Request handlers) -│ ├── models/ (Data layer) -│ ├── views/ (Templates) -│ ├── migrator/ (Database migrations) -│ ├── events/ (Application events) -│ ├── global/ (Global functions) -│ ├── mailers/ (Email components) -│ ├── jobs/ (Background jobs) -│ ├── lib/ (Custom libraries) -│ ├── plugins/ (Third-party plugins) -│ └── snippets/ (Code templates) -├── config/ (Configuration files) -│ ├── app.cfm (Application.cfc this scope settings) -│ ├── environment.cfm (Current environment) -│ ├── routes.cfm (URL routing) -│ ├── settings.cfm (Framework settings) -│ └── [environment]/ (Environment-specific overrides) -├── public/ (Web-accessible files) -│ ├── files/ (User uploads, sendFile() content) -│ ├── images/ (Image assets) -│ ├── javascripts/ (JavaScript files) -│ ├── stylesheets/ (CSS files) -│ ├── miscellaneous/ (Miscellaneous files) -│ ├── Application.cfc (Framework bootstrap) -│ └── index.cfm (Entry point) -├── tests/ (Test files) -├── vendor/ (Dependencies) -├── .env (Environment variables - NEVER commit) -├── box.json (Package configuration) -└── server.json (CommandBox server configuration) -``` - -## Development Commands - -### Code Generation -```bash -# Generate MVC components -wheels g model User name:string,email:string,active:boolean -wheels g controller Users index,show,new,create,edit,update,delete -wheels g view users/dashboard - -# Generate full CRUD scaffold -wheels g scaffold Product name:string,price:decimal,instock:boolean - -# Generate database migrations -wheels g migration CreateUsersTable -wheels g migration AddEmailToUsers --attributes="email:string:index" - -# Generate other components -wheels g mailer UserNotifications --methods="welcome,passwordReset" -wheels g job ProcessOrders --queue=high -wheels g test model User -wheels g helper StringUtils -``` - -### Migration Management -```bash -# Check migration status -wheels dbmigrate info - -# Migration to Latest -wheels dbmigrate latest - -# Migration to version 0 -wheels dbmigrate reset - -# Migration one version UP -wheels dbmigrate up - -# Migration one version DOWN -wheels dbmigrate down -``` - -### Server Management -```bash -# Start/stop development server -wheels server start -wheels server stop -wheels server restart - -# View server status -wheels server status - -# View server logs -wheels server log --follow -``` - -### Testing -```bash -# Run all tests -wheels test run -``` - -## Configuration Management - -### Environment Settings -Set your environment in `/config/environment.cfm`: -```cfm - - set(environment="development"); - -``` - -**Available Environments:** -- `development` - Local development with debug info -- `testing` - Automated testing environment -- `maintenance` - Maintenance mode with limited access -- `production` - Live production environment - -### Framework Settings -Configure global settings in `/config/settings.cfm`: -```cfm - - // Database configuration - set(dataSourceName="myapp-dev"); - set(dataSourceUserName="username"); - set(dataSourcePassword="password"); - - // URL rewriting - set(URLRewriting="On"); - - // Reload password - set(reloadPassword="mypassword"); - - // Error handling - set(showErrorInformation=true); - set(sendEmailOnError=false); - -``` - -### Environment-Specific Overrides -Create environment-specific settings in `/config/[environment]/settings.cfm`: -```cfm -// /config/production/settings.cfm - - set(dataSourceName="myapp-prod"); - set(showErrorInformation=false); - set(sendEmailOnError=true); - set(cachePages=true); - -``` - -## URL Routing - -### Default Route Pattern -URLs follow the pattern: `[controller]/[action]/[key]` - -**Examples:** -- `/users` → `Users.cfc`, `index()` action -- `/users/show/12` → `Users.cfc`, `show()` action, `params.key = 12` - -### Custom Routes -Define custom routes in `/config/routes.cfm`: -```cfm - -mapper() - // Named routes - .get(name="login", to="sessions##new") - .post(name="authenticate", to="sessions##create") - - // RESTful resources - .resources("users") - .resources("products", except="destroy") - - // Nested resources - use separate declarations - .resources("users") - .resources("orders") - - // Root route - .root(to="home##index", method="get") - - // Wildcard (keep last) - .wildcard() -.end(); - -``` - -### Route Helpers -```cfm -// Link generation -#linkTo(route="user", key=user.id, text="View User")# -#linkTo(controller="products", action="index", text="All Products")# - -// Form generation -#startFormTag(route="user", method="put", key=user.id)# - -// URL generation -#urlFor(route="users")# - -// Redirects in controllers -redirectTo(route="user", key=user.id); -``` - -## Model-View-Controller Patterns - -### Controller Structure -```cfm -component extends="Controller" { - - function config() { - // Filters for authentication/authorization - filters(through="authenticate", except="index"); - filters(through="findUser", only="show,edit,update,delete"); - - // Parameter verification - verifies(except="index,new,create", params="key", paramsTypes="integer"); - - // Content type support - provides("html,json"); - } - - function index() { - users = model("User").findAll(order="createdat DESC"); - } - - function create() { - user = model("User").new(params.user); - - if (user.save()) { - redirectTo(route="user", key=user.id, success="User created!"); - } else { - renderView(action="new"); - } - } - - private function authenticate() { - if (!session.authenticated) { - redirectTo(controller="sessions", action="new"); - } - } - - function sendWelcomeEmail() { - sendEmail( - template="users/welcome", - from="noreply@myapp.com", - to=user.email, - subject="Welcome to MyApp!", - user=user - ); - } - - function downloadReport() { - sendFile( - file="report.pdf", - name="Monthly Report.pdf", - type="application/pdf", - disposition="attachment", - directory="/reports/" - ); - } - - function requireSSL() { - if (!isSecure()) { - redirectTo(protocol="https"); - } - } -} -``` - -### Model Structure -```cfm -component extends="Model" { - - function config() { - // Associations - hasMany("orders"); - belongsTo("role"); - - // Validations - validatesPresenceOf("firstname,lastname,email"); - validatesUniquenessOf(property="email"); - validatesFormatOf(property="email", regEx="^[\w\.-]+@[\w\.-]+\.\w+$"); - - // Callbacks - beforeSave("hashPassword"); - afterCreate("sendWelcomeEmail"); - - // Nested properties for associations - nestedProperties(association="addresses", allowDelete=true, autoSave=true); - - // Custom finder methods (Wheels doesn't have scope() - use custom finder methods instead) - } - - function findByEmail(required string email) { - return findOne(where="email = '#arguments.email#'"); - } - - function findActive() { - return findAll(where="active = 1"); - } - - function findFirst() { - return findFirst(property="createdAt"); - } - - function fullName() { - return trim("#firstname# #lastname#"); - } - - function reload() { - // Reload this model instance from the database - return super.reload(); - } -} -``` - -### View Structure -```cfm - - - - - - - - #csrfMetaTags()# - #contentFor("title", "MyApp")# - #styleSheetLinkTag("application")# - - -
    - #flashMessages()# - #includeContent()# -
    - #javaScriptIncludeTag("application")# - - -
    - - - - -#contentFor("title", "Users")# - -

    Users

    -#linkTo(route="newUser", text="New User", class="btn btn-primary")# - - - - - - - - - - -
    #linkTo(route="user", key=users.id, text=users.firstname)##users.email# - #linkTo(route="editUser", key=users.id, text="Edit")# - #buttonTo(route="user", method="delete", key=users.id, - text="Delete", confirm="Are you sure?")# -
    - -

    No users found.

    -
    -
    -``` - -## Database Migrations - -### Migration Workflow -```bash -# Generate new migration -wheels g migration CreateUsersTable - -# Generate migration with attributes -wheels g migration AddEmailToUsers --attributes="email:string:index" - -# Run pending migrations -wheels dbmigrate latest - -# Rollback migrations -wheels dbmigrate down -``` - -### Migration Example -```cfm -component extends="wheels.migrator.Migration" { - - function up() { - transaction { - t = createTable(name="users", force=false); - t.string(columnNames="firstName,lastName", allowNull=false); - t.string(columnNames="email", limit=100, allowNull=false); - t.boolean(columnNames="active", default=true); - t.timestamps(); - t.create(); - - addIndex(table="users", columnNames="email", unique=true); - } - } - - function down() { - dropTable("users"); - } -} -``` - -### Column Types -```cfm -t.string(columnNames="name", limit=255, allowNull=false, default=""); -t.text(columnNames="description", allowNull=true); -t.integer(columnNames="count", allowNull=false, default=0); -t.decimal(columnNames="price", precision=10, scale=2); -t.boolean(columnNames="active", default=false); -t.date(columnNames="eventDate"); -t.datetime(columnNames="createdAt"); // Use for createdAt/updatedAt only when not using timestamps(); OK for other columns -t.timestamps(); // Creates createdAt and updatedAt -t.integer(columnNames="userId", allowNull=false); // Foreign key -``` - -### Advanced Migration Features - -```cfm -// Create database views -component extends="wheels.migrator.Migration" { - function up() { - v = createView(name="activeUsers"); - v.sql("SELECT id, name, email FROM users WHERE active = 1"); - v.create(); - } -} - -// Modify existing tables -component extends="wheels.migrator.Migration" { - function up() { - t = changeTable(name="users"); - t.string(columnNames="middleName", limit=100); - t.change(); - - // Add indexes - addIndex(table="users", columnNames="email", unique=true); - addIndex(table="users", columnNames="lastName,firstName"); - - // Rename tables - renameTable(oldName="user_profiles", newName="profiles"); - } - - function down() { - removeIndex(table="users", indexName="users_email"); - removeIndex(table="users", indexName="users_lastName_firstName"); - renameTable(oldName="profiles", newName="user_profiles"); - - t = changeTable(name="users"); - t.removeColumn(columnNames="middleName"); - t.change(); - } -} -``` - -## Testing - -### Test Structure -``` -tests/ -├── Test.cfc (Base test component) -├── controllers/ (Controller tests) -├── models/ (Model tests) -└── integration/ (Integration tests) -``` - -### Model Testing -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - // Setup for all tests in this spec - variables.testData = {}; - } - - function afterAll() { - // Cleanup after all tests - } - - function beforeEach() { - // Setup before each test - variables.user = ""; - } - - function afterEach() { - // Cleanup after each test - if (isObject(variables.user)) { - variables.user.delete(); - } - } - - function run() { - describe("User Model", function() { - - it("should be invalid when no data provided", function() { - var user = model("User").new(); - expect(user.valid()).toBeFalse("User should be invalid without data"); - expect(arrayLen(user.allErrors())).toBeGT(0, "Should have validation errors"); - }); - - it("should create user with valid data", function() { - var userData = { - firstname = "John", - lastname = "Doe", - email = "john@example.com" - }; - - var user = model("User").create(userData); - - expect(isObject(user)).toBeTrue("Should return user object"); - expect(user.valid()).toBeTrue("User should be valid"); - expect(user.firstname).toBe("John", "Should set firstname correctly"); - }); - }); - } -} -``` - -## Security Best Practices - -### CSRF Protection -```cfm -// In controllers -function config() { - protectsFromForgery(); // Enable CSRF protection -} - -// In forms -#startFormTag(route="user", method="put", key=user.id)# - #hiddenFieldTag("authenticityToken", authenticityToken())# - -#endFormTag()# - -// In layout head -#csrfMetaTags()# -``` - -### Input Validation -```cfm -// Parameter verification -function config() { - verifies(only="show,edit,update,delete", params="key", paramsTypes="integer"); - verifies(only="create,update", params="user", paramsTypes="struct"); -} - -// Model validation -function config() { - validatesPresenceOf("firstname,lastname,email"); - validatesFormatOf(property="email", regEx="^[\w\.-]+@[\w\.-]+\.\w+$"); - validatesLengthOf(property="password", minimum=8); -} -``` - -### Password Hashing - -**🔴 CRITICAL: Lucee/Adobe ColdFusion BCrypt Limitations** - -Lucee CFML does **not** support BCrypt hashing via the native `hash()` function. Attempting to use BCrypt will result in a runtime error: -``` -java.security.NoSuchAlgorithmException: bcrypt MessageDigest not available -``` - -**❌ DOES NOT WORK in Lucee:** -```cfm -passwordHash = hash(password, "BCrypt"); // Runtime error! -``` - -**✅ CORRECT APPROACH - Use SHA-256:** -```cfm -// For user registration -params.user.passwordHash = hash(params.user.password, "SHA-256"); - -user = model("User").create(params.user); -``` - -**✅ CORRECT APPROACH - Verify passwords:** -```cfm -private function verifyPassword(required string password, required string hash) { - return hash(arguments.password, "SHA-256") == arguments.hash; -} -``` - -**Available Hash Algorithms in Lucee:** -- ✅ `SHA-256` (recommended for passwords) -- ✅ `SHA-512` (even stronger) -- ✅ `MD5` (not recommended for passwords - too weak) -- ❌ `BCrypt` (NOT supported natively) - -**For Production BCrypt Support:** -If BCrypt is required, you must use a Java library: -```cfm -// Requires BCrypt Java library in classpath -bcrypt = createObject("java", "org.mindrot.jbcrypt.BCrypt"); -passwordHash = bcrypt.hashpw(password, bcrypt.gensalt()); -isValid = bcrypt.checkpw(password, passwordHash); -``` - -### SQL Injection Prevention -```cfm -// Use model methods (automatically sanitized) -users = model("User").findAll(where="email = '#params.email#'"); - -// Or use cfqueryparam in custom queries -sql = "SELECT * FROM users WHERE email = :email"; -users = queryExecute(sql, { email = { value = params.email, cfsqltype = "cf_sql_varchar" } }, {datasource = yourDatasourceName}); -``` - -## Performance Optimization - -### Caching -```cfm -// Page caching -function config() { - caches(action="index", time=30); // Cache for 30 minutes -} - -// Query caching -users = model("User").findAll(cache=60); // Cache for 60 minutes -``` - -### Database Optimization -```cfm -// Use includes to avoid N+1 queries -users = model("User").findAll(include="role,orders"); - -// Use select to limit columns -users = model("User").findAll(select="id,firstname,lastname,email"); - -// Use pagination -users = model("User").findAll(page=params.page, perPage=25); -``` - -## Deployment - -### Production Configuration -```cfm -// /config/production/settings.cfm - - // Database - set(dataSourceName="myapp-prod"); - - // Security - set(showErrorInformation=false); - set(sendEmailOnError=true); - - // Performance - set(cachePages=true); - set(cachePartials=true); - set(cacheQueries=true); - -``` - -### Environment Variables -Use `.env` file for sensitive configuration (never commit to version control): -```bash -# .env -DATABASE_URL=mysql://user:pass@localhost:3306/myapp_prod -SMTP_HOST=smtp.example.com -API_KEY=your-secret-api-key -``` - -Access in configuration: -```cfm - - if (FileExists(ExpandPath("/.env"))) { - set(dataSourceName=GetEnv("DATABASE_NAME")); - set(dataSourceUserName=GetEnv("DATABASE_USER")); - set(dataSourcePassword=GetEnv("DATABASE_PASSWORD")); - } - -``` - -## 🚨 MANDATORY: Native MCP Server - -**This Wheels application includes a native CFML MCP (Model Context Protocol) server that MUST be used by AI assistants for all development tasks.** - -**🔴 CRITICAL RULE: If `.mcp.json` exists, ALL development MUST use MCP tools - no exceptions.** - -The MCP server eliminates the need for Node.js dependencies and provides AI coding assistants with direct, integrated access to your Wheels application. - -### Accessing the MCP Server - -The MCP server is available at `/wheels/mcp` and supports: -- **Resources**: Documentation, guides, project context, patterns -- **Tools**: Code generation (models, controllers, views, migrations) -- **Prompts**: Context-aware help for Wheels development - -### MCP Client Configuration - -Configure your AI coding assistant to use the native MCP server: - -```json -{ - "mcpServers": { - "wheels": { - "type": "http", - "url": "http://localhost:8080/wheels/mcp" - } - } -} -``` - -Replace `8080` with your development server port. - -### Available Tools - -- `wheels_generate` - Generate components (models, controllers, etc.) -- `wheels_migrate` - Run database migrations -- `wheels_test` - Execute tests -- `wheels_server` - Manage development server -- `wheels_reload` - Reload application - -### Route Configuration - -The MCP server routes are pre-configured in the Wheels framework at `/vendor/wheels/public/routes.cfm`: - -```cfm -// Framework routes in wheels namespace -.get(name = "mcp", pattern = "mcp", to = "public##mcp") -.post(name = "mcpPost", pattern = "mcp", to = "public##mcp") -``` - -**IMPORTANT:** These are framework routes (in the `wheels` namespace) and should **NOT** be added to your application's `/config/routes.cfm`. The MCP server is automatically available at `/wheels/mcp` without any application configuration needed. - -#### Framework vs Application Routes - -**Framework Routes** (`/vendor/wheels/public/routes.cfm`): -- Pre-configured routes for Wheels internal functionality -- Include: `/wheels/mcp`, `/wheels/migrator`, `/wheels/api`, `/wheels/info`, etc. -- Automatically available in all Wheels applications -- Should NOT be duplicated in application routes - -**Application Routes** (`/config/routes.cfm`): -- Your custom application routes -- Business logic controllers and actions -- Custom API endpoints and resource routes -- This is where you define your application-specific routing - -## Common Patterns - -### Service Layer Pattern -```cfm -// /app/lib/UserService.cfc -component { - - function createUser(required struct userData) { - local.user = model("User").new(arguments.userData); - - transaction { - if (local.user.save()) { - sendWelcomeEmail(local.user); - return local.user; - } else { - transaction action="rollback"; - return false; - } - } - } -} -``` - -### API Development -```cfm -// API base controller -component extends="wheels.Controller" { - - function config() { - provides("json"); - filters(through="authenticate"); - } - - private function authenticate() { - // API authentication logic - } -} - -// API endpoint -function index() { - users = model("User").findAll(); - renderWith(data={users=users}); -} -``` - -### Error Handling -```cfm -// Global error handler in Application.cfc -function onError(exception, eventname) { - if (get("environment") == "production") { - WriteLog(file="application", text=exception.message, type="error"); - include "/app/views/errors/500.cfm"; - } else { - return true; // Let ColdFusion handle it - } -} -``` - -## Production-Tested Best Practices - -**The following patterns were validated during real-world Twitter clone development:** - -### Authentication Best Practices - -#### 1. Password Hashing - Lucee BCrypt Limitation (CRITICAL) - -**Problem:** Lucee CFML does not support BCrypt hashing natively via `hash()` function. - -**Discovery:** During Twitter clone development, attempting BCrypt resulted in: -``` -java.security.NoSuchAlgorithmException: bcrypt MessageDigest not available -``` - -**❌ WRONG - Causes Runtime Error:** -```cfm -function create() { - params.user.passwordHash = hash(params.user.password, "BCrypt"); // Runtime error! - user = model("User").create(params.user); -} -``` - -**✅ CORRECT - Use SHA-256:** -```cfm -function create() { - params.user.passwordHash = hash(params.user.password, "SHA-256"); - user = model("User").create(params.user); -} - -function verifyPassword(required string password, required string hash) { - return hash(arguments.password, "SHA-256") == arguments.hash; -} -``` - -**Alternative for Production:** -Use Java BCrypt library if stronger hashing is required: -```cfm -bcrypt = createObject("java", "org.mindrot.jbcrypt.BCrypt"); -passwordHash = bcrypt.hashpw(password, bcrypt.gensalt()); -``` - -### Migration Best Practices - -#### 1. Foreign Key Naming Conflicts (H2 Database) - -When creating multiple foreign keys to the same reference table, H2 database may generate duplicate constraint names. - -**Problem:** -```cfm -// Both try to create "FK_FOLLOWS_USERS" - conflict! -addForeignKey(table="follows", referenceTable="users", column="followerId") -addForeignKey(table="follows", referenceTable="users", column="followingId") -``` - -**Solution A: Explicit Key Names (Preferred)** -```cfm -addForeignKey( - table="follows", - referenceTable="users", - column="followerId", - referenceColumn="id", - keyName="FK_follows_follower", - onDelete="cascade" -); - -addForeignKey( - table="follows", - referenceTable="users", - column="followingId", - referenceColumn="id", - keyName="FK_follows_following", - onDelete="cascade" -); -``` - -**Solution B: Skip Foreign Keys for Self-Referential Tables** -```cfm -// For self-referential tables, rely on application-layer validation -// Indexes provide query performance, foreign keys are optional -addIndex(table="follows", columnNames="followerId,followingId", unique=true); -addIndex(table="follows", columnNames="followingId"); -// Note: Foreign keys omitted to avoid naming conflicts -``` - -#### 2. Composite Index Best Practices - -**Problem:** Creating individual indexes before composite unique index causes conflicts: -```cfm -// ❌ WRONG ORDER - causes duplicate index errors -addIndex(table="likes", columnNames="userId"); // Conflict! -addIndex(table="likes", columnNames="tweetId"); -addIndex(table="likes", columnNames="userId,tweetId", unique=true); -``` - -**Solution:** Create composite index FIRST (it covers the first column): -```cfm -// ✅ CORRECT ORDER -addIndex(table="likes", columnNames="userId,tweetId", unique=true); // First - covers userId queries too -addIndex(table="likes", columnNames="tweetId"); // Then add for second column queries -``` - -**Why:** A composite index on (userId, tweetId) can be used for queries filtering by userId alone, so a separate userId index is redundant. - -#### 3. Migration Retry Strategy - -When migrations fail mid-transaction, use `force=true` to clean up: - -```cfm -// Use force=true to drop and recreate if table exists from failed migration -t = createTable(name="likes", force=true); // Drops existing table first -``` - -**When to use:** -- After a failed migration leaves partial tables -- During development when iterating on schema -- NOT recommended for production (use proper migration versioning) - -### Model Best Practices - -#### 1. Correct Parameter Names - -**Validation Functions Use "properties" (plural):** -```cfm -// ✅ CORRECT -validatesPresenceOf(properties="username,email,passwordHash"); -validatesUniquenessOf(properties="username", message="Username already taken"); - -// ❌ WRONG -validatesPresenceOf(property="username"); // "property" doesn't exist -``` - -#### 2. Custom Validation Use "methods" (plural) - -```cfm -// ✅ CORRECT -validate(methods="preventSelfFollow"); - -// ❌ WRONG -validate(method="preventSelfFollow"); // "method" doesn't exist -``` - -### View Best Practices - -#### 1. Association Access in Query Loops - -**Problem:** Cannot call association methods directly on query columns: -```cfm - - #tweets.user()# ❌ Fails - tweets is a query, not an object - -``` - -**Solution:** Load object first, then access associations: -```cfm - - - ✅ Works! -

    By: #tweetUser.username#

    -
    -``` - -**Optimization:** Preload associations to avoid N+1 queries: -```cfm -// In controller - much better performance! -tweets = model("Tweet").findAll(include="user", order="createdAt DESC"); - - - #tweets.username# ✅ User data already joined - -``` - -### Controller Best Practices - -#### 1. RedirectTo with Back Parameter - -**Correct Syntax for Default Route:** -```cfm -// ✅ CORRECT - struct as string -redirectTo(back=true, default="{controller='tweets',action='index'}"); - -// ❌ WRONG - doesn't parse correctly -redirectTo(back=true, default="tweets##index"); -``` - -#### 2. Counter Updates with Like/Follow - -When implementing like/follow features, update counters atomically: - -```cfm -function create() { - like = model("Like").new(userId=session.userId, tweetId=params.tweetId); - - if (like.save()) { - // Increment counter on parent - tweet = model("Tweet").findByKey(key=params.tweetId); - tweet.update(likesCount=tweet.likesCount + 1); - } -} - -function delete() { - like = model("Like").findOne(where="userId = #session.userId# AND tweetId = #params.tweetId#"); - - if (isObject(like) && like.delete()) { - // Decrement counter, protect against negative - tweet = model("Tweet").findByKey(key=params.tweetId); - if (tweet.likesCount > 0) { - tweet.update(likesCount=tweet.likesCount - 1); - } - } -} -``` - -### Alpine.js Integration Best Practices - -#### 1. Character Counter with Visual Feedback - -```html -
    - - - - - / 280 - - - - -
    -``` - -#### 2. Auto-Dismissing Notifications - -```html -
    - Success message -
    -``` - -#### 3. Dropdown Menus with Click-Away - -```html -
    - - -
    - -
    -
    -``` - -**Important:** Add `x-cloak` CSS to prevent flash of unstyled content: -```css -[x-cloak] { display: none !important; } -``` - -### Database Seeding Best Practices - -For test data in migrations, use CFML date functions, NOT database functions: - -```cfm -// ✅ CORRECT - database-agnostic -var now = Now(); -var yesterday = DateAdd("d", -1, now); -var formatted = "TIMESTAMP '#DateFormat(yesterday, "yyyy-mm-dd")# #TimeFormat(yesterday, "HH:mm:ss")#'"; - -execute("INSERT INTO tweets (content, userId, createdAt, updatedAt) - VALUES ('Test tweet', 1, #formatted#, #formatted#)"); - -// ❌ WRONG - MySQL specific, won't work on H2/PostgreSQL -execute("INSERT INTO tweets (content, userId, createdAt) - VALUES ('Test tweet', 1, DATE_SUB(NOW(), INTERVAL 1 DAY))"); -``` - -## 🔴 Critical Learnings from Production Use - -**These patterns were discovered during actual Twitter clone development and must be followed:** - -### 1. Migration Parameter Types (CRITICAL) - -**Problem:** CLI-generated migrations use string values for boolean parameters, causing primary keys to fail. - -❌ **CLI Generates (WRONG):** -```cfm -t = createTable(name='users', force='false', id='true', primaryKey='id'); -``` - -✅ **Manual Fix Required:** -```cfm -// Option 1: Use defaults (RECOMMENDED) -t = createTable(name='users'); // Defaults: force=false, id=true - -// Option 2: Explicit booleans -t = createTable(name='users', force=false, id=true, primaryKey='id'); -``` - -**⚠️ MANDATORY:** Always review CLI-generated migrations and convert string booleans to proper boolean types. - -### 2. Model Primary Key Configuration (CRITICAL) - -**Problem:** "NoPrimaryKey" error even when migration creates `id` column correctly. - -✅ **ALWAYS Include in Every Model:** -```cfm -component extends="Model" { - function config() { - table("users"); - setPrimaryKey("id"); // ⚠️ REQUIRED - Must be explicit - - // Rest of configuration... - } -} -``` - -**Rule:** Even though Wheels creates an `id` column by default, you MUST explicitly call `setPrimaryKey("id")` in every model's `config()` method. - -### 3. Accessing Properties in beforeCreate() Callbacks (CRITICAL) - -**Problem:** "no accessible Member with name [PROPERTY]" error in beforeCreate callbacks. - -❌ **WRONG - Causes Runtime Error:** -```cfm -function setDefaults() { - if (!len(this.followersCount)) { - this.followersCount = 0; - } -} -``` - -✅ **CORRECT - Check Existence First:** -```cfm -function setDefaults() { - if (!structKeyExists(this, "followersCount") || !len(this.followersCount)) { - this.followersCount = 0; - } -} -``` - -**Rule:** In `beforeCreate()` and `beforeSave()` callbacks, always use `structKeyExists(this, "propertyName")` before accessing properties, as they may not exist yet. - -### 4. Validation Function Parameters (CRITICAL) - -**Problem:** Using singular "property" or "method" parameter names causes errors. - -❌ **WRONG:** -```cfm -validatesPresenceOf(property="username"); // Error! -validatesFormatOf(property="email", ...); // Error! -validate(method="customValidator"); // Error! -``` - -✅ **CORRECT - Always Use Plural:** -```cfm -validatesPresenceOf(properties="username,email,passwordHash"); -validatesUniquenessOf(properties="username,email"); -validatesFormatOf(properties="email", regEx="..."); -validatesLengthOf(properties="username", minimum=3, maximum=50); -validate(methods="customValidator"); -``` - -**Rule:** Wheels validation functions use "properties" (plural) and "methods" (plural) - never "property" or "method". - -### 5. CLI Generator Limitations - -**⚠️ CLI generators are helpful but have known issues:** - -1. **Migrations:** Generate with string booleans (must fix manually) -2. **Models:** Don't include `setPrimaryKey("id")` (must add manually) -3. **Callbacks:** Don't include `structKeyExists()` checks (must add manually) -4. **Validations:** May use correct syntax but verify parameter names - -**Workflow After CLI Generation:** -1. ✅ Run CLI generator -2. ✅ Review generated code -3. ✅ Fix boolean parameters in migrations -4. ✅ Add `setPrimaryKey("id")` to models -5. ✅ Add `structKeyExists()` to callbacks -6. ✅ Verify validation parameter names - -### 6. Migration Development Workflow - -**When migrations fail during development:** - -```bash -# 1. Fix the migration file -# 2. Reset database -wheels dbmigrate reset - -# 3. Re-run migrations -wheels dbmigrate latest -``` - -**Important:** -- ✅ Use `reset` during local development iteration -- ❌ NEVER use `force=true` in production migrations -- ✅ Always test migrations with fresh database reset - -### 7. H2 Database Considerations - -**The default Wheels development database (H2) has specific behaviors:** - -- **Case Sensitivity:** Column names may be case-insensitive in SQL but case-sensitive when accessing struct properties -- **Property Access:** Always use `structKeyExists()` for dynamic properties -- **Production Testing:** H2 is great for development but always test with your production database (MySQL/PostgreSQL) before deploying - -### 8. Model Configuration Template - -**Use this template for ALL new models to avoid common errors:** - -```cfm -component extends="Model" { - - function config() { - table("tablename"); - setPrimaryKey("id"); // ⚠️ ALWAYS INCLUDE - - // Associations - hasMany(name="children"); - belongsTo(name="parent"); - - // Validations - ALWAYS use "properties" (plural) - validatesPresenceOf(properties="field1,field2"); - validatesUniquenessOf(properties="field1"); - validatesFormatOf(properties="email", regEx="..."); - validatesLengthOf(properties="field1", minimum=1, maximum=50); - - // Custom validations - ALWAYS use "methods" (plural) - validate(methods="customValidator"); - - // Callbacks - beforeCreate("setDefaults"); - } - - // Callback with structKeyExists checks - function setDefaults() { - if (!structKeyExists(this, "counter") || !len(this.counter)) { - this.counter = 0; - } - } - - // Custom validation - function customValidator() { - if (structKeyExists(this, "field1") && /* condition */) { - addError(property="field1", message="Error message"); - } - } -} -``` - -### 9. Alpine.js Integration Pattern (Proven Working) - -**Character counter with dynamic button state (tested in production):** - -```html -
    - - - -
    - / -
    - - - -
    -``` - -**Don't forget the CSS:** -```css -[x-cloak] { display: none !important; } -``` - -### 10. Testing Sequence Best Practice - -**Follow this sequence for all browser testing:** - -1. ✅ Navigate to page and verify no errors -2. ✅ Take screenshot of initial state -3. ✅ Fill forms and test interactions -4. ✅ Verify Alpine.js features (counters, dropdowns, etc.) -5. ✅ Take screenshot after each major action -6. ✅ Verify success messages and redirects -7. ✅ Test delete/destroy actions -8. ✅ Check console for JavaScript errors - -## Common Issues and Troubleshooting - -### Association Errors -**"Missing argument name" in hasMany()** -This error occurs when mixing positional and named parameters in Wheels function calls: - -❌ **Incorrect (mixed parameter styles):** -```cfm -hasMany("comments", dependent="delete"); // Error: can't mix positional and named -``` - -✅ **Correct (consistent named parameters):** -```cfm -hasMany(name="comments", dependent="delete"); -``` - -✅ **Also correct (all positional):** -```cfm -hasMany("comments"); -``` - -Wheels requires consistent parameter syntax - either all positional or all named parameters. - -### Routing Issues -**Incorrect .resources() syntax** -Wheels resource routing syntax differs from Rails: - -❌ **Incorrect (Rails-style nested):** -```cfm -.resources("posts", function(nested) { - nested.resources("comments"); -}) -``` - -✅ **Correct (separate declarations):** -```cfm -.resources("posts") -.resources("comments") -``` - -**Route ordering matters:** resources → custom routes → root → wildcard - -### Form Helper Limitations -Wheels has more limited form helpers compared to Rails: - -❌ **Not available:** -```cfm -#emailField()# // Doesn't exist -#label(text="Name")# // text parameter not supported -``` - -✅ **Use instead:** -```cfm -#textField(type="email")# - -``` - -### Migration Data Seeding -Parameter binding in migrations can be unreliable. Use direct SQL: - -❌ **Problematic:** -```cfm -execute(sql="INSERT INTO posts (title) VALUES (?)", parameters=[{value=title}]); -``` - -✅ **Reliable:** -```cfm -execute("INSERT INTO posts (title, createdAt, updatedAt) VALUES ('My Post', NOW(), NOW())"); -``` - -### Debugging Tips -1. **Invoke `Skill("wheels-debugging")` when encountering errors** -2. Check Wheels documentation - don't assume Rails conventions work -3. Use simple patterns first, add complexity incrementally -4. Test associations and routes in isolation -5. Use `?reload=true` after configuration changes -6. Check debug footer for route information - -## Summary: Complete AI Assistant Workflow - -**🚨 MANDATORY: Follow this exact workflow for ALL Wheels development tasks** - -### The 5-Step Mandatory Process - -1. **🔧 Check MCP Tools** (STEP 1) - - Verify `.mcp.json` exists - - Test MCP server connection - - Confirm MCP tools are available - -2. **🎯 Invoke Claude Code Skill** (STEP 3) - - **ALWAYS FIRST** before code generation - - Select appropriate skill for task: - - Models → `Skill("wheels-model-generator")` - - Controllers → `Skill("wheels-controller-generator")` - - Views → `Skill("wheels-view-generator")` - - Migrations → `Skill("wheels-migration-generator")` - - Tests → `Skill("wheels-test-generator")` - - Auth → `Skill("wheels-auth-generator")` - - API → `Skill("wheels-api-generator")` - - Debugging → `Skill("wheels-debugging")` - - Refactoring → `Skill("wheels-refactoring")` - - Deployment → `Skill("wheels-deployment")` - - Wait for skill to load and provide guidance - - Follow skill's framework-specific patterns - -3. **📖 Load Documentation** (STEP 4, if needed) - - Read relevant `.ai/` documentation - - Or use MCP resources - - Validate against established patterns - -4. **💻 Generate Code with MCP Tools** (After skill validation) - - Use MCP tools exclusively (NO CLI commands) - - Follow skill guidance for proper patterns - - Examples: - - `mcp__wheels__wheels_generate(...)` - - `mcp__wheels__wheels_migrate(...)` - - `mcp__wheels__wheels_test()` - -5. **🌐 Test in Browser** (STEP 5 - MANDATORY) - - Check server status - - Navigate to application - - Screenshot homepage - - Test all user flows - - Verify interactive elements - - Document results - -### Critical Rules - -**✅ ALWAYS:** -- Invoke appropriate Claude Code skill BEFORE code generation -- Use MCP tools when `.mcp.json` exists -- Test in browser after ANY development work -- Follow Wheels-specific patterns from skills -- Validate against framework conventions - -**❌ NEVER:** -- Skip skill invocation for code generation -- Use CLI commands when MCP tools are available -- Skip browser testing -- Mix positional and named parameters in Wheels functions -- Assume Rails conventions work in Wheels -- Skip the anti-pattern detector skill - -### Example: Creating a Blog Post Feature - -```javascript -// ✅ CORRECT WORKFLOW -// 1. Check MCP -mcp__wheels__wheels_server(action="status") - -// 2. Model - Invoke skill FIRST -Skill("wheels-model-generator") -// Then generate: -mcp__wheels__wheels_generate(type="model", name="Post", attributes="title:string,content:text") - -// 3. Controller - Invoke skill FIRST -Skill("wheels-controller-generator") -// Then generate: -mcp__wheels__wheels_generate(type="controller", name="Posts", actions="index,show,new,create") - -// 4. Views - Invoke skill FIRST -Skill("wheels-view-generator") -// Then generate: -mcp__wheels__wheels_generate(type="view", name="posts/index") - -// 5. Migration - Invoke skill FIRST -Skill("wheels-migration-generator") -// Then migrate: -mcp__wheels__wheels_migrate(action="latest") - -// 6. Browser test (MANDATORY) - Use Playwright OR Puppeteer -// Playwright (preferred): -mcp__playwright__browser_navigate(url="http://localhost:8080/posts") -mcp__playwright__browser_snapshot() -mcp__playwright__browser_take_screenshot(filename="posts_index.png") - -// OR Puppeteer (alternative): -mcp__puppeteer__puppeteer_navigate(url="http://localhost:8080/posts") -mcp__puppeteer__puppeteer_screenshot(name="posts_index") -``` - -**Following this workflow ensures:** -- ✅ Proper Wheels conventions and patterns -- ✅ Prevention of common framework errors -- ✅ CFML syntax correctness -- ✅ Comprehensive browser validation -- ✅ Production-ready code quality \ No newline at end of file diff --git a/examples/tweet/app/CLAUDE.md b/examples/tweet/app/CLAUDE.md deleted file mode 100755 index fd8cf7d3a1..0000000000 --- a/examples/tweet/app/CLAUDE.md +++ /dev/null @@ -1,57 +0,0 @@ -# CLAUDE.md - Application Directory Dispatcher - -⚠️ **CRITICAL: All detailed documentation has been moved to the .ai folder!** - -## 🚨 MANDATORY: Before Working in /app Directory - -**The `/app` directory contains the core MVC components of your Wheels application.** - -### 📖 Component-Specific Documentation - -**ALWAYS read the appropriate documentation before working on any component:** - -#### 🏗️ Models (`/app/models/`) -**See:** `.ai/wheels/models/` for complete model documentation -- Data layer, ORM, associations, validations -- **Quick dispatcher:** `app/models/CLAUDE.md` - -#### 🎮 Controllers (`/app/controllers/`) -**See:** `.ai/wheels/controllers/` for complete controller documentation -- Request handling, filters, rendering, API development -- **Quick dispatcher:** `app/controllers/CLAUDE.md` - -#### 📄 Views (`/app/views/`) -**See:** `.ai/wheels/views/` for complete view documentation -- Templates, layouts, forms, partials, helpers -- **Quick dispatcher:** `app/views/CLAUDE.md` - -#### 🗄️ Database Migrations (`/app/migrator/`) -**See:** `.ai/wheels/database/migrations/` for migration documentation -- Schema changes, column types, indexes - -#### ⚙️ Other Components -- **Events** (`/app/events/`): Application lifecycle events -- **Global** (`/app/global/`): Globally accessible functions -- **Mailers** (`/app/mailers/`): Email components -- **Jobs** (`/app/jobs/`): Background job processing -- **Libraries** (`/app/lib/`): Custom libraries and utilities - -## 🔍 Critical Anti-Pattern Prevention - -**Before writing ANY code in the app directory:** -- [ ] ❌ **NO** mixed argument styles in Wheels functions -- [ ] ❌ **NO** ArrayLen() on model associations -- [ ] ❌ **NO** array loops on query objects -- [ ] ❌ **NO** Rails-style nested resource routing -- [ ] ✅ **YES** read component-specific .ai documentation -- [ ] ✅ **YES** follow established patterns from .ai documentation - -## 🚀 Quick Development Workflow - -1. **Generate component**: Use `wheels g` commands -2. **Read documentation**: Check appropriate `.ai/wheels/` folder -3. **Implement code**: Follow patterns from documentation -4. **Validate**: Check against anti-patterns -5. **Test**: Ensure functionality works correctly - -🚨 **DO NOT copy code examples from old CLAUDE.md files - read the complete .ai documentation!** \ No newline at end of file diff --git a/examples/tweet/app/controllers/CLAUDE.md b/examples/tweet/app/controllers/CLAUDE.md deleted file mode 100755 index a4b6a2496c..0000000000 --- a/examples/tweet/app/controllers/CLAUDE.md +++ /dev/null @@ -1,46 +0,0 @@ -# CLAUDE.md - Controllers Documentation Dispatcher - -⚠️ **CRITICAL: This content has been moved to comprehensive documentation!** - -## 🚨 MANDATORY: Before Working with Controllers - -**BEFORE implementing ANY controller code, you MUST read the complete documentation:** - -### 📖 Required Reading (IN ORDER) -1. **`.ai/wheels/troubleshooting/common-errors.md`** - PREVENT FATAL ERRORS -2. **`.ai/wheels/controllers/architecture.md`** - Controller fundamentals and CRUD -3. **`.ai/wheels/controllers/rendering.md`** - View rendering and responses -4. **`.ai/wheels/controllers/filters.md`** - Authentication and authorization -5. **`.ai/wheels/controllers/model-interactions.md`** - Controller-model patterns -6. **`.ai/wheels/controllers/best-practices.md`** - Controller development guidelines - -### 🔍 Quick Anti-Pattern Check -- [ ] ❌ **NO** mixed arguments: `renderText("error", status=404)` -- [ ] ❌ **NO** ArrayLen() on model results: `ArrayLen(users)` -- [ ] ✅ **YES** consistent arguments: ALL named OR ALL positional -- [ ] ✅ **YES** use .recordCount: `users.recordCount` -- [ ] ✅ **YES** plural naming: `UsersController.cfc` - -## 📚 Complete Controllers Documentation - -**All controller documentation is now located in:** `.ai/wheels/controllers/` - -The following files contain comprehensive controller guidance: -- `architecture.md` - Controller structure and CRUD patterns -- `rendering.md` - View rendering, redirects, flash messages -- `filters.md` - Authentication, authorization, data loading -- `model-interactions.md` - Controller-model patterns, validation -- `api.md` - JSON/XML APIs, authentication, versioning -- `security.md` - CSRF, parameter verification, sanitization -- `testing.md` - Controller testing patterns and helpers - -🚨 **DO NOT use code from this file - read the complete documentation first!** - -## Quick Generator Reference - -```bash -# Generate a new controller -wheels g controller Users index,show,new,create,edit,update,delete - -# After generation, ALWAYS read .ai/wheels/controllers/ documentation -``` \ No newline at end of file diff --git a/examples/tweet/app/events/CLAUDE.md b/examples/tweet/app/events/CLAUDE.md deleted file mode 100755 index 7a3f2c9a53..0000000000 --- a/examples/tweet/app/events/CLAUDE.md +++ /dev/null @@ -1,20 +0,0 @@ -# CLAUDE.md - Events Documentation Dispatcher - -⚠️ **Documentation moved to .ai folder!** - -## Application Events (`/app/events/`) - -**For complete events documentation:** -- **See:** `.ai/wheels/core-concepts/events/` (when available) -- **Main Documentation:** `.ai/wheels/` root documentation - -### Quick Reference -Application events handle lifecycle events like: -- `onapplicationstart.cfc` - Application initialization -- `onrequeststart.cfc` - Request preprocessing -- `onrequestend.cfc` - Request cleanup - -### Before Implementation -**ALWAYS read:** `.ai/wheels/troubleshooting/common-errors.md` first - -🚨 **Read complete .ai documentation before implementing events!** \ No newline at end of file diff --git a/examples/tweet/app/global/CLAUDE.md b/examples/tweet/app/global/CLAUDE.md deleted file mode 100755 index a1f0c5aeaa..0000000000 --- a/examples/tweet/app/global/CLAUDE.md +++ /dev/null @@ -1,18 +0,0 @@ -# CLAUDE.md - Global Functions Dispatcher - -⚠️ **Documentation moved to .ai folder!** - -## Global Functions (`/app/global/`) - -**For complete global functions documentation:** -- **See:** `.ai/wheels/core-concepts/global-functions/` (when available) -- **Main Documentation:** `.ai/wheels/` root documentation - -### Quick Reference -Global functions are accessible throughout your application: -- `functions.cfm` - Application-wide utility functions - -### Before Implementation -**ALWAYS read:** `.ai/wheels/troubleshooting/common-errors.md` first - -🚨 **Read complete .ai documentation before implementing global functions!** \ No newline at end of file diff --git a/examples/tweet/app/jobs/CLAUDE.md b/examples/tweet/app/jobs/CLAUDE.md deleted file mode 100755 index 1c29b5a16f..0000000000 --- a/examples/tweet/app/jobs/CLAUDE.md +++ /dev/null @@ -1,19 +0,0 @@ -# CLAUDE.md - Background Jobs Dispatcher - -⚠️ **Documentation moved to .ai folder!** - -## Background Jobs (`/app/jobs/`) - -**For complete jobs documentation:** -- **See:** `.ai/wheels/core-concepts/jobs/` (when available) -- **Main Documentation:** `.ai/wheels/` root documentation - -### Quick Reference -Background jobs handle asynchronous processing: -- Job classes for long-running tasks -- Queue management and processing - -### Before Implementation -**ALWAYS read:** `.ai/wheels/troubleshooting/common-errors.md` first - -🚨 **Read complete .ai documentation before implementing jobs!** \ No newline at end of file diff --git a/examples/tweet/app/lib/CLAUDE.md b/examples/tweet/app/lib/CLAUDE.md deleted file mode 100755 index 4c06bee98c..0000000000 --- a/examples/tweet/app/lib/CLAUDE.md +++ /dev/null @@ -1,20 +0,0 @@ -# CLAUDE.md - Libraries Dispatcher - -⚠️ **Documentation moved to .ai folder!** - -## Custom Libraries (`/app/lib/`) - -**For complete libraries documentation:** -- **See:** `.ai/wheels/core-concepts/libraries/` (when available) -- **Main Documentation:** `.ai/wheels/` root documentation - -### Quick Reference -Custom libraries contain reusable components: -- Service layer patterns -- Utility classes and functions -- Third-party integrations - -### Before Implementation -**ALWAYS read:** `.ai/wheels/troubleshooting/common-errors.md` first - -🚨 **Read complete .ai documentation before implementing libraries!** \ No newline at end of file diff --git a/examples/tweet/app/mailers/CLAUDE.md b/examples/tweet/app/mailers/CLAUDE.md deleted file mode 100755 index e23a4f8ff7..0000000000 --- a/examples/tweet/app/mailers/CLAUDE.md +++ /dev/null @@ -1,20 +0,0 @@ -# CLAUDE.md - Mailers Dispatcher - -⚠️ **Documentation moved to .ai folder!** - -## Email Mailers (`/app/mailers/`) - -**For complete mailers documentation:** -- **See:** `.ai/wheels/core-concepts/mailers/` (when available) -- **Main Documentation:** `.ai/wheels/` root documentation - -### Quick Reference -Email mailers handle application emails: -- User notifications and confirmations -- System alerts and reports -- Template-based email generation - -### Before Implementation -**ALWAYS read:** `.ai/wheels/troubleshooting/common-errors.md` first - -🚨 **Read complete .ai documentation before implementing mailers!** \ No newline at end of file diff --git a/examples/tweet/app/migrator/CLAUDE.md b/examples/tweet/app/migrator/CLAUDE.md deleted file mode 100755 index 8c36464f94..0000000000 --- a/examples/tweet/app/migrator/CLAUDE.md +++ /dev/null @@ -1,25 +0,0 @@ -# CLAUDE.md - Database Migrations Dispatcher - -⚠️ **Documentation moved to .ai folder!** - -## Database Migrations (`/app/migrator/`) - -**For complete migrations documentation:** -- **See:** `.ai/wheels/database/migrations/` for comprehensive migration docs -- **Main Documentation:** `.ai/wheels/` root documentation - -### Quick Reference -Database migrations handle schema changes: -- Table creation and modification -- Index management -- Data seeding (use direct SQL) - -### Before Implementation -**ALWAYS read:** `.ai/wheels/troubleshooting/common-errors.md` first - -### Critical Migration Patterns -- ✅ Use direct SQL for data seeding (not parameter binding) -- ✅ Wrap operations in transactions -- ✅ Implement both up() and down() methods - -🚨 **Read complete .ai documentation before implementing migrations!** \ No newline at end of file diff --git a/examples/tweet/app/models/CLAUDE.md b/examples/tweet/app/models/CLAUDE.md deleted file mode 100755 index acc59f2e5e..0000000000 --- a/examples/tweet/app/models/CLAUDE.md +++ /dev/null @@ -1,49 +0,0 @@ -# CLAUDE.md - Models Documentation Dispatcher - -⚠️ **CRITICAL: This content has been moved to comprehensive documentation!** - -## 🚨 MANDATORY: Before Working with Models - -**BEFORE implementing ANY model code, you MUST read the complete documentation:** - -### 📖 Required Reading (IN ORDER) -1. **`.ai/wheels/troubleshooting/common-errors.md`** - PREVENT FATAL ERRORS -2. **`.ai/wheels/models/data-handling.md`** - Critical query vs array patterns -3. **`.ai/wheels/models/architecture.md`** - Model fundamentals and structure -4. **`.ai/wheels/models/associations.md`** - Relationship patterns (CRITICAL) -5. **`.ai/wheels/models/validations.md`** - Validation methods and patterns -6. **`.ai/wheels/models/best-practices.md`** - Model development guidelines - -### 🔍 Quick Anti-Pattern Check -- [ ] ❌ **NO** mixed arguments: `hasMany("comments", dependent="delete")` -- [ ] ❌ **NO** ArrayLen() on associations: `ArrayLen(user.posts())` -- [ ] ✅ **YES** consistent arguments: `hasMany(name="comments", dependent="delete")` -- [ ] ✅ **YES** use .recordCount: `user.posts().recordCount` - -## 📚 Complete Models Documentation - -**All model documentation is now located in:** `.ai/wheels/models/` - -The following files contain comprehensive model guidance: -- `architecture.md` - Model structure and fundamentals -- `associations.md` - Relationships and foreign keys -- `validations.md` - Validation rules and methods -- `callbacks.md` - Lifecycle hooks and events -- `methods-reference.md` - Complete method documentation -- `advanced-patterns.md` - Complex model examples -- `user-authentication.md` - Authentication model patterns -- `testing.md` - Model testing strategies -- `performance.md` - Query optimization -- `best-practices.md` - Development guidelines -- `advanced-features.md` - Timestamps and dirty tracking - -🚨 **DO NOT use code from this file - read the complete documentation first!** - -## Quick Generator Reference - -```bash -# Generate a new model -wheels g model User name:string,email:string,active:boolean - -# After generation, ALWAYS read .ai/wheels/models/ documentation -``` \ No newline at end of file diff --git a/examples/tweet/app/snippets/CLAUDE.md b/examples/tweet/app/snippets/CLAUDE.md deleted file mode 100755 index 455ece8564..0000000000 --- a/examples/tweet/app/snippets/CLAUDE.md +++ /dev/null @@ -1,27 +0,0 @@ -# CLAUDE.md - Code Snippets Dispatcher - -⚠️ **Documentation moved to .ai folder!** - -## Code Snippets (`/app/snippets/`) - -**For complete snippets documentation:** -- **See:** `.ai/wheels/snippets/` for code templates and examples -- **Main Documentation:** `.ai/wheels/` root documentation - -### Quick Reference -Code snippets provide reusable templates: -- Model templates and patterns -- Controller action templates -- View helper templates -- Common code patterns - -### Before Implementation -**ALWAYS read:** `.ai/wheels/troubleshooting/common-errors.md` first - -### Available Snippet Categories -- Model snippets (associations, validations) -- Controller snippets (CRUD, filters) -- View snippets (forms, layouts) -- Configuration snippets - -🚨 **Read complete .ai documentation before using snippets!** \ No newline at end of file diff --git a/examples/tweet/app/views/CLAUDE.md b/examples/tweet/app/views/CLAUDE.md deleted file mode 100755 index 63bd75bbf8..0000000000 --- a/examples/tweet/app/views/CLAUDE.md +++ /dev/null @@ -1,59 +0,0 @@ -# CLAUDE.md - Views Documentation Dispatcher - -⚠️ **CRITICAL: This content has been moved to comprehensive documentation!** - -## 🚨 MANDATORY: Before Working with Views - -**BEFORE implementing ANY view code, you MUST read the complete documentation:** - -### 📖 Required Reading (IN ORDER) -1. **`.ai/wheels/troubleshooting/common-errors.md`** - PREVENT FATAL ERRORS -2. **`.ai/wheels/views/data-handling.md`** - CRITICAL query vs array patterns -3. **`.ai/wheels/views/architecture.md`** - View structure and conventions -4. **`.ai/wheels/views/forms.md`** - Form helpers and limitations (CRITICAL) -5. **`.ai/wheels/views/layouts.md`** - Layout patterns and inheritance -6. **`.ai/wheels/views/best-practices.md`** - View implementation checklist - -### 🔍 Quick Anti-Pattern Check -- [ ] ❌ **NO** ArrayLen() on queries: `ArrayLen(posts)` -- [ ] ❌ **NO** array loops on queries: `` -- [ ] ❌ **NO** emailField() or passwordField() (don't exist) -- [ ] ✅ **YES** use .recordCount: `posts.recordCount` -- [ ] ✅ **YES** query loops: `` -- [ ] ✅ **YES** textField() with type: `textField(type="email")` - -## 📚 Complete Views Documentation - -**All view documentation is now located in:** `.ai/wheels/views/` - -The following files contain comprehensive view guidance: -- `data-handling.md` - Critical query vs array patterns -- `architecture.md` - View structure and file organization -- `layouts.md` - Layout patterns and inheritance -- `partials.md` - Partial usage and patterns -- `forms.md` - Form helpers and Wheels limitations -- `helpers.md` - View helpers and custom helpers -- `advanced-patterns.md` - AJAX, performance, caching -- `testing.md` - View testing patterns -- `best-practices.md` - Implementation checklist and patterns - -🚨 **DO NOT use code from this file - read the complete documentation first!** - -### ⚡ Critical Wheels View Patterns - -**REMEMBER:** Wheels associations return QUERIES, not arrays: -```cfm - - - -

    #posts.title#

    -
    -
    - - - - -

    #post.title#

    -
    -
    -``` \ No newline at end of file diff --git a/examples/tweet/config/CLAUDE.md b/examples/tweet/config/CLAUDE.md deleted file mode 100755 index 8d5fe5f0e9..0000000000 --- a/examples/tweet/config/CLAUDE.md +++ /dev/null @@ -1,842 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with configuration files in a Wheels application. - -## 🚨 CRITICAL: PRE-CONFIGURATION IMPLEMENTATION CHECKLIST 🚨 - -### 🛑 MANDATORY DOCUMENTATION READING (COMPLETE BEFORE ANY CODE) - -**YOU MUST READ THESE FILES IN ORDER:** - -✅ **Step 1: Error Prevention (ALWAYS FIRST)** -- [ ] `.ai/wheels/troubleshooting/common-errors.md` - PREVENT FATAL ERRORS -- [ ] `.ai/wheels/patterns/validation-templates.md` - CONFIG CHECKLIST - -✅ **Step 2: Configuration-Specific Documentation** -- [ ] `.ai/wheels/core-concepts/routing/basics.md` - Routing fundamentals -- [ ] `.ai/wheels/core-concepts/routing/resources.md` - Resource routing -- [ ] `.ai/wheels/configuration/environments.md` - Environment settings - -### 🔴 CRITICAL ROUTING ANTI-PATTERNS (MOST COMMON CONFIG ERRORS) - -**Before writing ANY routing code, verify you will NOT:** -- [ ] ❌ Use Rails-style nested resources: `.resources("posts", function(nested) { nested.resources("comments"); })` -- [ ] ❌ Put wildcard route before other routes -- [ ] ❌ Mix argument styles in route definitions -- [ ] ❌ Forget to call `.end()` to close mapper - -**And you WILL:** -- [ ] ✅ Use separate resource declarations: `.resources("posts").resources("comments")` -- [ ] ✅ Put routes in correct order: resources → custom → root → wildcard -- [ ] ✅ Use consistent argument syntax throughout -- [ ] ✅ Always close mapper with `.end()` - -### 📋 ROUTING IMPLEMENTATION TEMPLATE (MANDATORY STARTING POINT) - -```cfm - -mapper() - // 1. Resource routes FIRST - .resources("posts") - .resources("comments") - .resources("users") - - // 2. Custom routes SECOND - .get(name="login", to="sessions##new") - .post(name="authenticate", to="sessions##create") - .delete(name="logout", to="sessions##delete") - - // 3. Root route THIRD - .root(to="posts##index", method="get") - - // 4. Wildcard route LAST (ALWAYS LAST) - .wildcard() -.end(); // CRITICAL: Always end with .end() - -``` - -### 🔍 POST-IMPLEMENTATION VALIDATION (REQUIRED BEFORE COMPLETION) - -**After writing configuration code, you MUST:** - -```bash -# 1. Syntax validation -wheels server start --validate - -# 2. Route testing -wheels server start -# Test routes in browser to ensure they work - -# 3. Manual verification -# Check that routes.cfm follows correct ordering -# Verify all mappers end with .end() -``` - -**Manual checklist verification:** -- [ ] Resources declared separately (not nested) -- [ ] Routes in correct order (resources, custom, root, wildcard) -- [ ] Mapper closed with .end() -- [ ] No mixed argument styles -- [ ] Wildcard route is last - -## Overview - -The `/config` directory contains all configuration files for your Wheels application. In Wheels 3.0.0+, configuration was moved from `/app/config` to the root-level `/config` directory for better organization and clearer separation of concerns. These files control application behavior, database connections, routing, environment settings, and framework-wide defaults. - -## File Structure and Purpose - -### Core Configuration Files -``` -/config/ -├── app.cfm (Application.cfc this scope settings) -├── environment.cfm (Current environment setting) -├── routes.cfm (URL routing configuration) -├── settings.cfm (Global framework settings) -└── [environment]/ - ├── development/ - │ └── settings.cfm (Development environment overrides) - ├── testing/ - │ └── settings.cfm (Testing environment overrides) - ├── maintenance/ - │ └── settings.cfm (Maintenance mode overrides) - └── production/ - └── settings.cfm (Production environment overrides) -``` - -### File Descriptions - -**`app.cfm`** - Contains `this` scope variables for Application.cfc (application name, session settings, datasources, etc.) - -**`environment.cfm`** - Sets the current environment (`development`, `testing`, `maintenance`, `production`) - -**`settings.cfm`** - Global framework settings and defaults that apply across all environments - -**`routes.cfm`** - URL routing configuration using the mapper DSL - -**`[environment]/settings.cfm`** - Environment-specific overrides for global settings - -## Application Configuration (`app.cfm`) - -Configure Application.cfc `this` scope variables: - -```cfm - - /* - Use this file to set variables for the Application.cfc's "this" scope. - */ - - // Application identity - this.name = "MyWheelsApp"; - this.applicationTimeout = CreateTimeSpan(1,0,0,0); - - // Session management - this.sessionManagement = true; - this.sessionTimeout = CreateTimeSpan(0,0,30,0); - - // Security settings - this.secureJSON = true; - this.secureJSONPrefix = "//"; - - // Custom tag paths - this.customTagPaths = ListAppend( - this.customTagPaths, - ExpandPath("../customtags") - ); - - // Datasource configuration - this.datasources['myapp'] = { - class: 'com.mysql.cj.jdbc.Driver', - connectionString: 'jdbc:mysql://localhost:3306/myapp?useSSL=false', - username: 'dbuser', - password: 'dbpass' - }; - - // H2 Database example (for development) - this.datasources['myapp-dev'] = { - class: 'org.h2.Driver', - connectionString: 'jdbc:h2:file:./db/h2/myapp-dev;MODE=MySQL', - username: 'sa' - }; - -``` - -### Common Application Settings -- **Application identity**: `this.name`, `this.applicationTimeout` -- **Session management**: `this.sessionManagement`, `this.sessionTimeout` -- **Client management**: `this.clientManagement`, `this.clientStorage` -- **Security**: `this.secureJSON`, `this.scriptProtect`, `this.sessionRotate` -- **Datasources**: `this.datasources` struct -- **Mappings**: `this.mappings` struct -- **Custom paths**: `this.customTagPaths`, `this.componentPaths` - -## Environment Configuration (`environment.cfm`) - -Sets the current environment mode: - -```cfm - -// Use this file to set the current environment for your application. -// You can set it to "development", "testing", "maintenance" or "production". -// Don't forget to issue a reload request (e.g. reload=true) after making changes. - -set(environment = "development"); - -``` - -### Environment Modes - -**Development** -- Shows detailed errors on screen -- No error email notifications -- Basic caching (config, schema, routes, images) -- Most convenient for active development - -**Production** -- Full caching enabled (actions, pages, queries, partials) -- Custom error pages shown -- Error email notifications enabled -- Fastest performance mode - -**Testing** -- Same caching as production -- Error handling like development -- Good for testing at production speed with debugging - -**Maintenance** -- Shows maintenance page to users -- Exceptions for specific IPs/user agents -- Useful for deploying updates - -### Environment Switching - -**Permanent switch**: Edit `/config/environment.cfm` then reload -```bash -# URL reload after file change -http://myapp.com/?reload=true -``` - -**Temporary switch via URL**: -```bash -# Switch to testing mode temporarily -http://myapp.com/?reload=testing - -# With password protection -http://myapp.com/?reload=production&password=mypass -``` - -**Disable URL switching** (recommended for production): -```cfm -set(allowEnvironmentSwitchViaUrl = false); -``` - -## Framework Settings (`settings.cfm`) - -Global framework configuration using `set()` function: - -```cfm - - /* - Use this file to configure your application. - Environment-specific files can override these settings. - */ - - // Database configuration - set(dataSourceName = "myapp"); - set(dataSourceUserName = "dbuser"); - set(dataSourcePassword = "dbpass"); - set(coreTestDataSourceName = "myapp_test"); - - // URL rewriting - set(URLRewriting = "On"); // "On", "Partial", or "Off" - - // Security - set(reloadPassword = "mySecurePassword123"); - - // Error handling - set(showDebugInformation = true); - set(showErrorInformation = true); - set(sendEmailOnError = false); - set(errorEmailAddress = "admin@myapp.com"); - - // Caching settings - set(cacheActions = true); - set(cachePages = true); - set(cachePartials = true); - set(cacheQueries = true); - set(defaultCacheTime = 60); - - // ORM settings - set(tableNamePrefix = ""); - set(automaticValidations = true); - set(timeStampOnCreateProperty = "createdAt"); - set(timeStampOnUpdateProperty = "updatedAt"); - set(softDeleteProperty = "deletedAt"); - - // Function defaults - set(functionName = "findAll", perPage = 25); - set(functionName = "linkTo", encode = false); - - // Asset settings - set(assetQueryString = true); - set(assetPaths = { - http: "cdn1.myapp.com,cdn2.myapp.com", - https: "secure-cdn.myapp.com" - }); - -``` - -### Configuration Categories - -#### Database Settings -```cfm -set(dataSourceName = "myapp"); -set(dataSourceUserName = "user"); -set(dataSourcePassword = "pass"); -set(tableNamePrefix = "app_"); -``` - -#### URL and Routing Settings -```cfm -set(URLRewriting = "On"); -set(obfuscateUrls = false); -set(loadDefaultRoutes = true); -``` - -#### Caching Settings -```cfm -set(cacheActions = true); -set(cachePages = true); -set(cachePartials = true); -set(cacheQueries = true); -set(cacheRoutes = true); -set(defaultCacheTime = 60); -set(maximumItemsToCache = 5000); -``` - -#### Security Settings -```cfm -set(reloadPassword = "securePassword"); -set(csrfStore = "session"); // or "cookie" -set(allowCorsRequests = false); -``` - -#### Error and Debug Settings -```cfm -set(showDebugInformation = true); -set(showErrorInformation = true); -set(sendEmailOnError = false); -set(errorEmailAddress = "admin@example.com"); -set(errorEmailSubject = "App Error"); -``` - -#### ORM Settings -```cfm -set(automaticValidations = true); -set(timeStampOnCreateProperty = "createdAt"); -set(timeStampOnUpdateProperty = "updatedAt"); -set(softDeleteProperty = "deletedAt"); -set(setUpdatedAtOnCreate = true); -set(transactionMode = "commit"); -``` - -#### Function Default Overrides -```cfm -// Set global defaults for any Wheels function -set(functionName = "findAll", perPage = 20); -set(functionName = "textField", class = "form-control"); -set(functionName = "linkTo", encode = true); -``` - -#### Asset Management -```cfm -set(assetQueryString = true); -set(assetPaths = { - http: "assets1.example.com,assets2.example.com", - https: "secure-assets.example.com" -}); -``` - -#### Plugin Settings -```cfm -set(overwritePlugins = false); -set(deletePluginDirectories = false); -set(loadIncompatiblePlugins = false); -``` - -## Routing Configuration (`routes.cfm`) - -Define URL patterns and map them to controller actions: - -```cfm - - // Use this file to add routes to your application - // Don't forget to reload after changes: ?reload=true - // See https://wheels.dev/3.0.0/guides/handling-requests-with-controllers/routing - - mapper() - // Resource-based routing (recommended) - .resources("users") - .resources("posts") - .resources("comments") // Nested resources use separate declarations - - // Singular resource (no primary key in URL) - .resource("profile") - .resource("cart") - - // Custom routes - .get(name="search", pattern="search", to="search##index") - .get(name="about", pattern="about", to="pages##about") - .post(name="contact", pattern="contact", to="contact##create") - - // API routes with namespace - .namespace("api", { - resources: ["users", "posts"] - }) - - // Catch-all wildcard routing - .wildcard() - - // Root route (homepage) - .root(to="home##index", method="get") - .end(); - -``` - -### Routing Patterns - -#### Resource Routing -```cfm -.resources("products") -// Creates: index, show, new, create, edit, update, delete actions - -.resources("categories", { - except: ["delete"] -}) -.resources("products") // Nested resources declared separately -``` - -#### Custom Routes -```cfm -.get(name="productSearch", pattern="products/search", to="products##search") -.post(name="newsletter", pattern="newsletter/signup", to="newsletter##signup") -.patch(name="activate", pattern="users/[key]/activate", to="users##activate") -.delete(name="clearCart", pattern="cart/clear", to="cart##clear") -``` - -#### Route Constraints -```cfm -.get(name="userPosts", pattern="users/[userId]/posts/[postId]", to="posts##show", { - constraints: { - userId: "\d+", - postId: "\d+" - } -}) -``` - -#### Route Parameters -- **`[key]`** - Primary key parameter (maps to `params.key`) -- **`[slug]`** - URL-friendly identifier -- **`[any-name]`** - Custom parameter name -- **Optional parameters**: Use `?` suffix like `[category?]` - -### Route Helper Usage -After defining routes, use them in views: - -```cfm - -#linkTo(route="products", text="All Products")# -#linkTo(route="newProduct", text="New Product")# -#linkTo(route="product", key=product.id, text="View Product")# -#linkTo(route="editProduct", key=product.id, text="Edit")# - - -#linkTo(route="search", text="Search Products")# -#linkTo(route="about", text="About Us")# - - -#linkTo(route="userPosts", userId=user.id, postId=post.id)# -``` - -### Routing Best Practices - -#### Route Ordering -Routes are processed in order - first match wins. Order routes from most specific to most general: - -```cfm -mapper() - // 1. Resource routes first - .resources("posts") - .resources("comments") - - // 2. Custom routes - .get(name="search", pattern="search", to="search##index") - .get(name="admin", pattern="admin", to="admin##dashboard") - - // 3. Root route - .root(to="posts##index", method="get") - - // 4. Wildcard routing last - .wildcard() -.end(); -``` - -#### Common Routing Mistakes - -**❌ Incorrect nested resource syntax:** -```cfm -.resources("posts", function(nested) { - nested.resources("comments"); // This doesn't work in Wheels -}) -``` - -**✅ Correct approach - separate declarations:** -```cfm -.resources("posts") -.resources("comments") -``` - -**❌ Wrong route ordering:** -```cfm -mapper() - .wildcard() // Too early - catches everything - .resources("posts") // Never reached -.end(); -``` - -**✅ Correct ordering:** -```cfm -mapper() - .resources("posts") // Specific routes first - .wildcard() // Catch-all last -.end(); -``` - -#### Route Testing -Always test routes after changes: -1. Use `?reload=true` to reload configuration -2. Check the debug footer "Routes" link to view all routes -3. Test both positive and negative cases -4. Verify route helpers generate correct URLs - -## Environment-Specific Settings - -Override global settings per environment in `/config/[environment]/settings.cfm`: - -### Development Settings (`/config/development/settings.cfm`) -```cfm - -// Development-specific settings -set(showDebugInformation = true); -set(showErrorInformation = true); -set(sendEmailOnError = false); - -// Disable caching for easier development -set(cacheActions = false); -set(cachePages = false); -set(cachePartials = false); - -// Use development database -set(dataSourceName = "myapp_dev"); - -// Debug-friendly asset handling -set(assetQueryString = false); - -``` - -### Production Settings (`/config/production/settings.cfm`) -```cfm - -// Production optimizations -set(showDebugInformation = false); -set(showErrorInformation = false); -set(sendEmailOnError = true); -set(errorEmailAddress = "alerts@myapp.com"); - -// Full caching enabled -set(cacheActions = true); -set(cachePages = true); -set(cachePartials = true); -set(cacheQueries = true); -set(defaultCacheTime = 120); - -// Production database -set(dataSourceName = "myapp_production"); - -// CDN configuration -set(assetPaths = { - http: "cdn.myapp.com", - https: "secure-cdn.myapp.com" -}); - -// Security hardening -set(reloadPassword = "verySecureProductionPassword"); -set(allowEnvironmentSwitchViaUrl = false); - -``` - -### Testing Settings (`/config/testing/settings.cfm`) -```cfm - -// Testing environment -set(showDebugInformation = true); -set(sendEmailOnError = false); - -// Use test database -set(dataSourceName = "myapp_test"); - -// Fast caching but visible errors -set(cacheActions = true); -set(cacheQueries = true); - -``` - -## Configuration Best Practices - -### 1. Environment-Appropriate Settings -```cfm -// Different settings per environment -// Development: debugging on, caching off -// Production: debugging off, caching on, error emails -// Testing: production-like caching with development-like error reporting -``` - -### 2. Secure Sensitive Data -```cfm -// Use environment variables for sensitive config -set(dataSourcePassword = GetEnvironmentValue("DB_PASSWORD")); -set(reloadPassword = GetEnvironmentValue("RELOAD_PASSWORD")); - -// Or use encrypted configuration files -``` - -### 3. Database Configuration Patterns -```cfm -// Multiple datasources -this.datasources['primary'] = { /* main db */ }; -this.datasources['analytics'] = { /* analytics db */ }; -this.datasources['cache'] = { /* cache db */ }; - -// Environment-specific datasources -this.datasources['myapp'] = { - class: GetEnvironmentValue("DB_CLASS", "org.h2.Driver"), - connectionString: GetEnvironmentValue("DB_URL", "jdbc:h2:file:./db/app"), - username: GetEnvironmentValue("DB_USER", "sa"), - password: GetEnvironmentValue("DB_PASSWORD", "") -}; -``` - -### 4. Performance Tuning -```cfm -// Production performance settings -set(cacheActions = true); -set(cachePages = true); -set(cachePartials = true); -set(cacheQueries = true); -set(defaultCacheTime = 60); -set(maximumItemsToCache = 10000); - -// Development convenience settings -set(cacheActions = false); -set(cacheControllerConfig = false); -set(cacheModelConfig = false); -``` - -### 5. Function Defaults Organization -```cfm -// Group related function defaults -// Form defaults -set(functionName = "textField", class = "form-control"); -set(functionName = "textArea", class = "form-control"); -set(functionName = "select", class = "form-select"); - -// Pagination defaults -set(functionName = "findAll", perPage = 25); -set(functionName = "paginationLinks", class = "pagination"); - -// Link defaults -set(functionName = "linkTo", encode = true); -set(functionName = "buttonTo", class = "btn btn-primary"); -``` - -## Configuration Loading Order - -Wheels loads configuration in this order: - -1. **Framework defaults** - Built-in Wheels defaults -2. **`/config/settings.cfm`** - Global application settings -3. **`/config/[environment]/settings.cfm`** - Environment-specific overrides -4. **URL parameters** - Temporary overrides via `?reload=environment` - -Later settings override earlier ones. - -## Accessing Configuration Values - -### In Controllers/Models/Views -```cfm -// Get configuration values -environment = get("environment"); -datasource = get("dataSourceName"); -debugMode = get("showDebugInformation"); - -// Conditional logic based on environment -if (get("environment") == "development") { - // Development-only code -} - -// Check if setting exists -if (hasSettingValue("customSetting")) { - customValue = get("customSetting"); -} -``` - -### In Application Events -```cfm -// In /app/events/onapplicationstart.cfm -if (get("environment") == "production") { - // Initialize production services - initializeLogging(); - initializeMonitoring(); -} -``` - -## Common Configuration Patterns - -### Multi-Environment Database Setup -```cfm -// In /config/settings.cfm (base config) -set(dataSourceName = "myapp"); - -// In /config/development/settings.cfm -set(dataSourceName = "myapp_dev"); - -// In /config/testing/settings.cfm -set(dataSourceName = "myapp_test"); - -// In /config/production/settings.cfm -set(dataSourceName = "myapp_prod"); -``` - -### Feature Flags -```cfm -// In /config/settings.cfm -set(enableNewFeature = false); - -// In /config/development/settings.cfm -set(enableNewFeature = true); - -// In controllers/views -if (get("enableNewFeature")) { - // Show new feature -} -``` - -### API Configuration -```cfm -// API endpoints per environment -// Development -set(apiBaseUrl = "http://localhost:3000/api"); -set(apiKey = "dev-key-123"); - -// Production -set(apiBaseUrl = "https://api.myapp.com/v1"); -set(apiKey = GetEnvironmentValue("PROD_API_KEY")); -``` - -### Email Configuration -```cfm -// Email settings per environment -// Development - log emails, don't send -set(sendEmailOnError = false); -set(mailMethod = "file"); -set(mailPath = "./logs/mail"); - -// Production - send real emails -set(sendEmailOnError = true); -set(errorEmailAddress = "alerts@myapp.com"); -set(mailMethod = "smtp"); -set(mailServer = "smtp.myapp.com"); -``` - -## Migration from Pre-3.0 - -### Breaking Change in Wheels 3.0.0 -Configuration moved from `/app/config` to `/config` at root level. - -### Automated Migration -```bash -# Use CommandBox recipe to migrate automatically -box recipe https://raw.githubusercontent.com/wheels-dev/wheels/develop/cli/recipes/config-migration.boxr -``` - -### Manual Migration Steps -1. Move `/app/config` to `/config` -2. Update `Application.cfc` path references -3. Update any hardcoded paths in application code -4. Update deployment scripts and documentation -5. Test that configuration loads correctly - -## Troubleshooting Configuration - -### Common Issues - -**Configuration not loading**: -- Check file syntax for CFML errors -- Verify file permissions -- Ensure proper `` tags - -**Settings not taking effect**: -- Issue reload: `?reload=true` -- Check environment-specific overrides -- Verify setting name spelling - -**Route conflicts**: -- Check route order (first match wins) -- Use route debugging: Click "Routes" in debug footer -- Verify pattern syntax - -**Database connection issues**: -- Verify datasource configuration -- Check database driver availability -- Test connection in CF Admin - -### Debug Configuration -```cfm -// Dump all settings (development only) - - -// Check specific setting -Environment: #get("environment")# -Datasource: #get("dataSourceName")# -Debug Mode: #get("showDebugInformation")# -``` - -## Security Considerations - -### Production Hardening -```cfm -// Disable environment switching via URL -set(allowEnvironmentSwitchViaUrl = false); - -// Strong reload password -set(reloadPassword = "VerySecureRandomPassword123!"); - -// Error email configuration -set(sendEmailOnError = true); -set(excludeFromErrorEmail = "password,ssn,creditCard"); - -// Disable debug information -set(showDebugInformation = false); -set(showErrorInformation = false); -``` - -### Sensitive Data Protection -```cfm -// Use environment variables for secrets -set(dataSourcePassword = GetEnvironmentValue("DB_PASSWORD")); -set(apiKey = GetEnvironmentValue("API_SECRET")); - -// Encrypt configuration files containing sensitive data -// Store encryption keys outside application directory -``` - -This configuration system provides flexible, environment-aware settings management that scales from development through production deployment. \ No newline at end of file diff --git a/examples/tweet/plugins/CLAUDE.md b/examples/tweet/plugins/CLAUDE.md deleted file mode 100755 index 07285966a5..0000000000 --- a/examples/tweet/plugins/CLAUDE.md +++ /dev/null @@ -1,906 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with plugins in a Wheels application. - -## Overview - -Plugins are the recommended way to extend Wheels functionality without modifying the core framework. They allow you to add new functions, override existing ones, or create complete standalone features. Wheels uses a plugin architecture that keeps the core lightweight while enabling powerful extensions through community contributions. - -## Plugin Architecture - -### Purpose and Philosophy -- **Core remains lightweight**: Only essential functionality in Wheels core -- **Community extensions**: Plugins provide additional features -- **Easy distribution**: Zip files for simple installation/removal -- **Version compatibility**: Plugins specify compatible Wheels versions -- **Mixins system**: Functions injected into appropriate Wheels objects - -### Plugin Lifecycle -1. **Development**: Create plugin CFC and interface files -2. **Packaging**: Zip plugin files with version number -3. **Distribution**: Publish to forgebox.io repository -4. **Installation**: Drop zip in `/plugins` folder -5. **Loading**: Wheels automatically extracts and loads on startup -6. **Injection**: Plugin functions mixed into framework objects - -## Plugin Structure - -### Required Files - -**`PluginName.cfc`** - Main plugin component: -```cfm -component { - function init() { - this.version = "2.0,3.0"; // Compatible Wheels versions - return this; - } - - // Plugin functions here - public string function myNewFunction() { - return "Hello from plugin!"; - } -} -``` - -**`index.cfm`** - Plugin user interface (shown in debug area): -```cfm - -

    My Plugin #application.wheels.pluginMeta["myPlugin"]["version"]#

    -

    This plugin adds amazing functionality to your Wheels app.

    - -

    Usage Examples:

    -
    -##myNewFunction()##
    -##linkTo(text="Click Me", action="index", customAttribute="value")##
    -
    -
    -``` - -**`box.json`** - Package metadata (required for publishing): -```json -{ - "name": "My Amazing Plugin", - "version": "1.0.0", - "author": "Your Name", - "location": "username/repo-name#v1.0.0", - "directory": "/plugins/", - "createPackageDirectory": true, - "packageDirectory": "MyAmazingPlugin", - "slug": "my-amazing-plugin", - "type": "wheels-plugins", - "shortDescription": "Adds amazing functionality", - "keywords": "wheels,plugin,amazing" -} -``` - -### Plugin Packaging -```bash -# File structure -MyPlugin/ -├── MyPlugin.cfc # Main component -├── index.cfm # UI interface -├── box.json # Package metadata -└── README.md # Documentation - -# Zip as: MyPlugin-1.0.0.zip -``` - -## Plugin Development - -### Basic Plugin Template -```cfm -component { - /** - * Initialize plugin - REQUIRED method - */ - function init() { - this.version = "3.0"; // Wheels version compatibility - return this; - } - - /** - * Add new functionality - */ - public string function myHelper() { - return "Custom functionality"; - } - - /** - * Override existing function - */ - public string function timeAgoInWords() { - // Call original function and modify result - return core.timeAgoInWords(argumentCollection=arguments) & " (approximately)"; - } - - /** - * Private plugin function (use $ prefix) - */ - private string function $internalHelper() { - return "Internal use only"; - } -} -``` - -### Plugin Attributes - -#### Mixin Targeting -Control where plugin functions are injected: - -```cfm - - - - - - -component { - public string function controllerOnly() mixin="controller" { - return "Only available in controllers"; - } - - public string function modelOnly() mixin="model" { - return "Only available in models"; - } - - public string function everywhere() { - return "Available everywhere (default)"; - } -} -``` - -**Available mixin targets**: -- `controller` - Controllers and views -- `model` - Model objects -- `dispatch` - Dispatch object -- `global` - Global functions -- `application` - Application scope -- `none` - No injection -- `microsoftsqlserver`, `mysql`, `oracle`, `postgresql` - Database adapters - -#### Environment Targeting -Restrict plugin loading to specific environments: - -```cfm - - - - - - - -``` - -#### Plugin Dependencies -Specify required plugins: - -```cfm - - - -``` - -### Function Override Patterns - -#### Extending Core Functions -```cfm -component { - /** - * Override linkTo to add custom attributes - */ - public string function linkTo() { - // Extract custom arguments - local.customClass = ""; - if (StructKeyExists(arguments, "customClass")) { - local.customClass = arguments.customClass; - StructDelete(arguments, "customClass"); - } - - // Call original function - local.result = core.linkTo(argumentCollection=arguments); - - // Modify result if needed - if (len(local.customClass)) { - local.result = Replace(local.result, 'class="', 'class="' & local.customClass & ' '); - } - - return local.result; - } -} -``` - -#### Complete Function Replacement -```cfm -component { - /** - * Completely override a function - */ - public string function singularize() { - // Custom singularization logic - return "$$completelyOverridden"; - } - - /** - * Override with fallback to core - */ - public string function pluralize() { - // Custom logic first, then core fallback - if (someCondition) { - return customPluralize(argumentCollection=arguments); - } - return core.pluralize(argumentCollection=arguments); - } -} -``` - -### Stand-Alone Plugins -Plugins that work independently without mixing into framework: - -```cfm -component { - function init() { - this.version = "3.0"; - return this; - } - - public any function processData(required string input) { - // Standalone functionality - return processInput(arguments.input); - } -} -``` - -Access from views via plugin object: -```cfm - - - #myPlugin.processData("test data")# - -``` - -## Development Workflow - -### Local Development Setup -Configure development-friendly settings in `/config/development/settings.cfm`: - -```cfm - -// Prevent plugin files from being overwritten by zip -set(overwritePlugins = false); - -// Prevent plugin directories from being deleted -set(deletePluginDirectories = false); - -// Allow incompatible plugins for testing -set(loadIncompatiblePlugins = true); - -``` - -### Development Process -1. **Create plugin directory**: `/plugins/MyPlugin/` -2. **Develop plugin files**: `MyPlugin.cfc`, `index.cfm` -3. **Test locally**: Reload app with `?reload=true` -4. **Package for distribution**: Create zip file -5. **Publish to forgebox**: Use CommandBox publishing workflow - -### Testing Plugins -```cfm - -component { - function init() { - this.version = "3.0"; - return this; - } - - /** - * Test function for development - */ - public string function testFunction() { - return "Plugin is working: " & Now(); - } -} -``` - -Test in views: -```cfm - - Plugin test: #testFunction()# - -``` - -## Plugin Installation - -### Manual Installation -1. Download plugin zip file -2. Place in `/plugins/` directory -3. Reload application: `?reload=true` -4. Plugin automatically extracts and loads - -### CommandBox Installation -```bash -# List available plugins -wheels plugins list - -# Install specific plugin -box install shortcodes - -# Install specific version -box install my-plugin@1.2.0 - -# Install from GitHub -box install username/repo-name -``` - -### Plugin Management Commands -```bash -# Plugin information -wheels plugins info pluginName - -# List installed plugins -wheels plugins list --installed - -# Check for updates -wheels plugins outdated - -# Update plugin -wheels plugins update pluginName - -# Update all plugins -wheels plugins update-all - -# Remove plugin -wheels plugins remove pluginName -``` - -## Plugin Examples - -### Simple Helper Plugin -```cfm -component { - function init() { - this.version = "3.0"; - return this; - } - - /** - * Format currency with custom options - * [section: View Helpers] - * [category: Formatting] - */ - public string function formatCurrency(required numeric amount, string symbol="$") { - return arguments.symbol & NumberFormat(arguments.amount, "0.00"); - } - - /** - * Generate random string - * [section: Utilities] - * [category: String] - */ - public string function randomString(numeric length=8) { - local.chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - local.result = ""; - - for (local.i = 1; local.i <= arguments.length; local.i++) { - local.randomChar = Mid(local.chars, RandRange(1, Len(local.chars)), 1); - local.result &= local.randomChar; - } - - return local.result; - } -} -``` - -### Form Enhancement Plugin -```cfm -component mixin="controller" { - function init() { - this.version = "3.0"; - return this; - } - - /** - * Enhanced buttonTo with confirmation - */ - public string function buttonTo() { - local.confirm = $extractConfirm(arguments); - local.result = core.buttonTo(argumentCollection=arguments); - - if (Len(local.confirm)) { - local.result = $addJSConfirm(local.result, local.confirm, "input"); - } - - return local.result; - } - - /** - * Enhanced linkTo with confirmation - */ - public string function linkTo() { - local.confirm = $extractConfirm(arguments); - local.result = core.linkTo(argumentCollection=arguments); - - if (Len(local.confirm)) { - local.result = $addJSConfirm(local.result, local.confirm, "a"); - } - - return local.result; - } - - /** - * Extract and remove confirm argument - */ - private string function $extractConfirm(required struct args) { - local.confirm = ""; - if (StructKeyExists(arguments.args, "confirm")) { - local.confirm = arguments.args.confirm; - StructDelete(arguments.args, "confirm"); - } - return local.confirm; - } - - /** - * Add JavaScript confirmation to HTML - */ - private string function $addJSConfirm(required string html, required string confirm, required string tag) { - local.js = "return confirm('#JSStringFormat(arguments.confirm)#');"; - local.onclick = ' onclick="' & local.js & '"'; - - // Find tag position and add onclick - local.tagPos = Find("<" & arguments.tag & " ", arguments.html); - if (local.tagPos) { - local.closePos = Find(">", arguments.html, local.tagPos); - local.result = Insert(local.onclick, arguments.html, local.closePos - 1); - } else { - local.result = arguments.html; - } - - return local.result; - } -} -``` - -### Model Enhancement Plugin -```cfm -component mixin="model" { - function init() { - this.version = "3.0"; - return this; - } - - /** - * Add slug functionality to models - */ - public string function generateSlug(required string text) { - local.slug = LCase(arguments.text); - local.slug = REReplace(local.slug, "[^a-z0-9\s]", "", "all"); - local.slug = REReplace(local.slug, "\s+", "-", "all"); - local.slug = REReplace(local.slug, "^-+|-+$", "", "all"); - return local.slug; - } - - /** - * Auto-slug functionality in beforeSave callback - */ - public void function beforeSave() { - // Auto-generate slug from title if empty - if (hasProperty("slug") && hasProperty("title") && !Len(property("slug"))) { - property(name="slug", value=generateSlug(property("title"))); - } - - // Call any existing beforeSave - if (StructKeyExists(this, "$originalBeforeSave")) { - $originalBeforeSave(); - } - } -} -``` - -### Flash Messages Plugin -```cfm -component { - function init() { - this.version = "3.0"; - return this; - } - - /** - * Override flashMessages for Bootstrap styling - */ - public string function flashMessages() { - local.result = core.flashMessages(argumentCollection=arguments); - - if (Len(local.result)) { - // Remove outer div - local.result = Replace(local.result, '
    ', ''); - local.result = Replace(local.result, '
    ', ''); - - // Convert p tags to Bootstrap alerts - local.result = Replace(local.result, '', '
    ', 'all'); - - // Add Bootstrap classes and dismiss button - local.append = ' role="alert">'; - - local.result = Replace(local.result, 'class="success-message">', 'class="alert alert-success alert-dismissible"' & local.append, 'all'); - local.result = Replace(local.result, 'class="error-message">', 'class="alert alert-danger alert-dismissible"' & local.append, 'all'); - local.result = Replace(local.result, 'class="info-message">', 'class="alert alert-info alert-dismissible"' & local.append, 'all'); - local.result = Replace(local.result, 'class="warning-message">', 'class="alert alert-warning alert-dismissible"' & local.append, 'all'); - } - - return local.result; - } -} -``` - -## Plugin Publishing - -### ForgeBox Setup -```bash -# Register for forgebox account -forgebox register - -# Or login with existing account -forgebox login - -# Verify login -forgebox whoami -``` - -### Package Configuration -Create comprehensive `box.json`: - -```json -{ - "name": "MyAwesome Plugin", - "version": "1.0.0", - "author": "Your Name", - "location": "username/repo-name#v1.0.0", - "directory": "/plugins/", - "createPackageDirectory": true, - "packageDirectory": "MyAwesomePlugin", - "slug": "my-awesome-plugin", - "type": "wheels-plugins", - "homepage": "https://github.com/username/repo-name", - "shortDescription": "Adds awesome functionality to Wheels", - "keywords": "wheels,plugin,awesome,helper", - "private": false, - "scripts": { - "postVersion": "package set location='username/repo-name#v`package version`'", - "patch-release": "bump --patch && publish", - "minor-release": "bump --minor && publish", - "major-release": "bump --major && publish", - "postPublish": "!git push --follow-tags" - } -} -``` - -### Publishing Workflow -```bash -# Initial setup in plugin directory -git init -git add . -git commit -m "Initial plugin version" -git remote add origin https://github.com/username/repo-name.git -git push -u origin master - -# Publishing releases -run-script patch-release # 1.0.0 -> 1.0.1 -run-script minor-release # 1.0.0 -> 1.1.0 -run-script major-release # 1.0.0 -> 2.0.0 -``` - -This automatically: -- Updates version in `box.json` -- Creates git tag -- Pushes to GitHub -- Publishes to forgebox - -### Manual Publishing -```bash -# Publish current version -publish - -# Publish specific version -publish 1.2.3 - -# Unpublish version -unpublish 1.2.3 --force -``` - -## Advanced Plugin Patterns - -### Plugin with Java Libraries -```cfm -component { - function init() { - this.version = "3.0"; - // Java libs automatically mapped from plugin directory - return this; - } - - public any function processWithJava() { - // Use Java classes from plugin's .jar files - local.processor = CreateObject("java", "com.myplugin.Processor"); - return local.processor.process(argumentCollection=arguments); - } -} -``` - -### Plugin Configuration -```cfm -component { - function init() { - this.version = "3.0"; - - // Set plugin defaults - set(functionName="myPlugin", defaultOption="value"); - - return this; - } - - public string function myPlugin() { - // Use configured defaults - local.option = get("myPluginDefaultOption"); - return processWithOption(local.option); - } -} -``` - -### Plugin Callbacks -```cfm -component { - function init() { - this.version = "3.0"; - return this; - } - - /** - * Called when plugin is loaded - */ - public void function onLoad() { - // Plugin initialization logic - initializePlugin(); - } - - /** - * Called when application reloads - */ - public void function onReload() { - // Reload-specific logic - clearPluginCache(); - } -} -``` - -### Testing Integration -Create test files in plugin: - -```cfm - -component extends="BaseSpec" { - function run() { - describe("MyPlugin", function() { - it("should format currency correctly", function() { - expect(formatCurrency(123.45)).toBe("$123.45"); - expect(formatCurrency(123.45, "€")).toBe("€123.45"); - }); - - it("should generate random strings", function() { - local.result = randomString(10); - expect(len(local.result)).toBe(10); - }); - }); - } -} -``` - -### CI/CD Integration -`.travis.yml` for automated testing: - -```yaml -language: java -sudo: required -jdk: - - oraclejdk8 - -before_install: - - sudo apt-key adv --keyserver keys.gnupg.net --recv 6DA70622 - - sudo echo "deb http://downloads.ortussolutions.com/debs/noarch /" | sudo tee -a /etc/apt/sources.list.d/commandbox.list - -install: - - sudo apt-get update && sudo apt-get --assume-yes install CommandBox - - box version - - box install wheels-cli - - box install wheels-dev/wheels - - box install username/my-plugin - -before_script: - - box server start lucee5 - -script: > - testResults="$(box wheels test type=myplugin servername=lucee5)"; - echo "$testResults"; - if ! grep -i "\Tests Complete: All Good!" <<< $testResults; then exit 1; fi - -notifications: - email: true -``` - -## Plugin Security and Best Practices - -### Security Considerations -```cfm -component { - /** - * Validate input in plugin functions - */ - public string function processData(required string input) { - // Validate and sanitize input - if (!isValid("string", arguments.input)) { - throw(message="Invalid input provided"); - } - - // Escape output if generating HTML - return EncodeForHtml(processInput(arguments.input)); - } - - /** - * Use private functions for internal logic - */ - private string function $validateInput(required string input) { - // Private validation logic - return arguments.input; - } -} -``` - -### Performance Best Practices -```cfm -component { - /** - * Cache expensive operations - */ - public string function expensiveOperation(required string input) { - local.cacheKey = "plugin_operation_" & Hash(arguments.input); - - if (get("cacheQueries")) { - local.result = cacheGet(local.cacheKey); - if (isDefined("local.result")) { - return local.result; - } - } - - local.result = performExpensiveOperation(arguments.input); - - if (get("cacheQueries")) { - cachePut(local.cacheKey, local.result, CreateTimeSpan(0, 1, 0, 0)); - } - - return local.result; - } -} -``` - -### Documentation Standards -```cfm -component { - /** - * Process user input data - * - * [section: Plugins] - * [category: Data Processing] - * - * @input The raw input string to process - * @format Output format (html, text, xml) - * @validate Whether to validate input (default: true) - * @return Processed string in specified format - */ - public string function processData( - required string input, - string format="html", - boolean validate=true - ) { - // Implementation here - } -} -``` - -## Plugin Management - -### Version Compatibility -```cfm -component { - function init() { - // Multiple version support - this.version = "2.0,3.0,3.1"; - return this; - } -} -``` - -### Plugin Dependencies -```cfm -component dependency="BaseUtilities,DateHelpers" { - function init() { - this.version = "3.0"; - - // Check dependencies are loaded - if (!StructKeyExists(application.wheels.plugins, "BaseUtilities")) { - throw(message="BaseUtilities plugin required"); - } - - return this; - } -} -``` - -### Plugin Settings Management -```cfm -// In /config/settings.cfm -set(overwritePlugins = false); // Don't overwrite during development -set(deletePluginDirectories = false); // Don't delete plugin folders -set(loadIncompatiblePlugins = true); // Load plugins with version warnings -set(showIncompatiblePlugins = true); // Show compatibility warnings -``` - -### Plugin Debugging -```cfm -component { - function init() { - this.version = "3.0"; - - // Debug mode check - if (get("environment") == "development") { - writeLog(text="MyPlugin loaded successfully", file="application"); - } - - return this; - } - - public string function debugFunction() { - if (get("showDebugInformation")) { - return "Plugin debug info: " & SerializeJSON(getPluginInfo()); - } - return ""; - } -} -``` - -## Plugin Distribution - -### File Organization -``` -MyPlugin/ -├── MyPlugin.cfc # Main component -├── index.cfm # Plugin interface -├── box.json # Package metadata -├── README.md # Documentation -├── CHANGELOG.md # Version history -├── LICENSE # License file -├── tests/ # Test files -│ └── MyPluginTest.cfc -├── docs/ # Additional documentation -├── lib/ # Java libraries (.jar files) -└── assets/ # CSS/JS/Images -``` - -### ZIP Packaging -```bash -# Create distribution zip -zip -r MyPlugin-1.0.0.zip MyPlugin/ -x "*.git*" "*tests*" -``` - -### Installation Verification -Users can verify plugin installation: - -```cfm - - -

    MyPlugin is loaded

    -

    Version: #application.wheels.pluginMeta["MyPlugin"]["version"]#

    - -

    MyPlugin not found

    -
    -``` - -Plugins provide a powerful way to extend Wheels functionality while maintaining a clean separation from the core framework. They enable community contributions, code reuse, and customization without compromising the framework's lightweight philosophy. \ No newline at end of file diff --git a/examples/tweet/public/CLAUDE.md b/examples/tweet/public/CLAUDE.md deleted file mode 100755 index 5a9502fed2..0000000000 --- a/examples/tweet/public/CLAUDE.md +++ /dev/null @@ -1,753 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with the public directory in a Wheels application. - -## Overview - -The `/public` directory serves as the document root for your Wheels application, containing static assets (CSS, JavaScript, images), system files, and the framework bootstrap. It's the only directory directly accessible by web browsers, providing security by keeping application code outside the web root. This directory contains both user assets and essential framework files that enable Wheels to function properly. - -## Directory Structure - -### Standard Public Directory Layout -``` -public/ -├── index.cfm (Framework bootstrap - DO NOT MODIFY) -├── Application.cfc (Framework bootstrap - DO NOT MODIFY) -├── urlrewrite.xml (URL rewriting configuration) -├── favicon.ico (Site favicon) -├── robots.txt (Search engine directives) -├── sitemap.xml (Search engine sitemap) -├── files/ (User file uploads/downloads) -├── images/ (Image assets) -├── javascripts/ (JavaScript files) -├── stylesheets/ (CSS files) -└── miscellaneous/ (Non-framework code) - └── Application.cfc (Empty Application.cfc) -``` - -### Asset Directories - -**`stylesheets/`** - CSS files and stylesheets -- Conventional location for CSS assets -- Accessed via `styleSheetLinkTag()` helper -- Supports nested directories for organization - -**`javascripts/`** - JavaScript files and libraries -- Conventional location for JavaScript assets -- Accessed via `javaScriptIncludeTag()` helper -- Can contain libraries, frameworks, and custom code - -**`images/`** - Image assets and graphics -- Conventional location for images -- Accessed via `imageTag()` helper -- Wheels automatically detects image dimensions and caches metadata - -**`files/`** - File storage and downloads -- General file storage accessible via web -- Used with `sendFile()` function for file delivery -- Suitable for user uploads and downloadable content - -**`miscellaneous/`** - Non-framework CFML code -- Contains empty `Application.cfc` to bypass Wheels -- Used for standalone CFML code that must run outside framework -- Ideal for Flash AMF bindings, AJAX proxies, or legacy integrations - -## Framework Bootstrap Files - -### Core System Files (DO NOT MODIFY) - -**`index.cfm`** - Application entry point: -```cfm - - - -#application.wheels.dispatch.$request()# -``` - -**`Application.cfc`** - Framework initialization: -- Sets up application mappings and paths -- Loads Wheels framework and dependencies -- Handles environment variables and configuration -- Manages Java library loading for plugins -- Provides application lifecycle event handling - -**Key Application.cfc Features:** -- **Path Mapping**: Maps `/app`, `/vendor`, `/wheels`, `/config`, `/plugins` -- **Environment Loading**: Loads `.env` files with variable interpolation -- **Plugin Integration**: Automatically maps Java libraries from plugins -- **Session Management**: Enables sessions by default for Flash scope -- **Request Lifecycle**: Handles all ColdFusion application events -- **Reload Mechanism**: Supports application reloading via URL parameters - -### URL Rewriting Configuration - -**`urlrewrite.xml`** - Tuckey URL Rewrite configuration: -```xml - - - Wheels pretty URLs - ^/(cf_script|flex2gateway|jrunscripts|CFIDE/administrator|lucee/admin|cfformgateway|cffileservlet|lucee|files|images|javascripts|miscellaneous|stylesheets|wheels/public/assets|robots.txt|favicon.ico|sitemap.xml|index.cfm) - ^/(.*)$ - /index.cfm/$1 - - - - - Convert dot to format parameter - ^/(.*)\\.(\\w+)$ - /$1?format=$2 - - -``` - -**Configuration Notes:** -- Used with Tomcat/CommandBox for URL rewriting -- Excludes static asset directories from rewriting -- Converts `/users/123.json` to `/users/123?format=json` -- Can be safely deleted if not using Tuckey/CommandBox - -## Asset Management - -### CSS Stylesheets - -**Conventional Structure:** -``` -stylesheets/ -├── application.css (Main application styles) -├── admin.css (Admin section styles) -├── print.css (Print styles) -├── mobile.css (Mobile styles) -├── vendor/ (Third-party CSS) -│ ├── bootstrap.min.css -│ ├── fontawesome.min.css -│ └── daterangepicker.css -└── components/ (Component-specific styles) - ├── forms.css - ├── navigation.css - └── tables.css -``` - -**Usage in Views:** -```cfm - -#styleSheetLinkTag("application")# - - -#styleSheetLinkTag("application,admin,print")# - - -#styleSheetLinkTag(source="print", media="print")# - - -#styleSheetLinkTag("https://cdn.example.com/styles.css")# - - -#styleSheetLinkTag("vendor/bootstrap.min")# -``` - -### JavaScript Files - -**Conventional Structure:** -``` -javascripts/ -├── application.js (Main application JavaScript) -├── admin.js (Admin functionality) -├── vendor/ (Third-party libraries) -│ ├── jquery.min.js -│ ├── bootstrap.min.js -│ ├── moment.min.js -│ └── daterangepicker.js -├── components/ (Component-specific JS) -│ ├── forms.js -│ ├── navigation.js -│ └── datatables.js -└── pages/ (Page-specific JS) - ├── users.js - └── products.js -``` - -**Usage in Views:** -```cfm - -#javaScriptIncludeTag("application")# - - -#javaScriptIncludeTag("vendor/jquery.min,vendor/bootstrap.min,application")# - - -#javaScriptIncludeTag("https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js")# - - -#javaScriptIncludeTag("vendor/moment.min")# -``` - -### Images - -**Conventional Structure:** -``` -images/ -├── logo.png (Site logo) -├── favicon.ico (Browser icon) -├── backgrounds/ (Background images) -│ ├── hero.jpg -│ └── pattern.png -├── icons/ (Icon images) -│ ├── user.png -│ ├── admin.png -│ └── settings.png -├── products/ (Product images) -│ ├── product-001.jpg -│ └── product-002.jpg -└── uploads/ (User uploaded images) - └── avatars/ -``` - -**Usage in Views:** -```cfm - -#imageTag("logo.png")# - - -#imageTag(source="logo.png", alt="Company Logo")# - - -#imageTag(source="hero.jpg", width="800", height="400", class="img-responsive")# - - -#imageTag("backgrounds/hero.jpg")# -``` - -**Image Helper Features:** -- Automatically detects image dimensions -- Caches image metadata for performance -- Generates proper `alt` attributes from filename -- Supports nested directory structures - -## File Management - -### File Storage and Downloads - -**Files Directory Structure:** -``` -files/ -├── downloads/ (Public downloadable files) -│ ├── user-manual.pdf -│ ├── price-list.xlsx -│ └── software/ -│ └── installer.zip -├── uploads/ (User uploaded files) -│ ├── documents/ -│ ├── images/ -│ └── temp/ -└── exports/ (Generated exports) - ├── reports/ - └── data/ -``` - -**File Delivery with sendFile():** -```cfm - -function downloadManual() { - sendFile(file="files/downloads/user-manual.pdf", name="User Manual.pdf"); -} - -function downloadReport() { - // Generate file path - filePath = "files/exports/reports/monthly-report-" & DateFormat(Now(), "yyyy-mm") & ".pdf"; - - // Send file with custom name - sendFile(file=filePath, name="Monthly Report.pdf", type="application/pdf"); -} -``` - -**File Upload Handling:** -```cfm - - -#startFormTag(action="uploadFile", enctype="multipart/form-data")# - #fileField(objectName="document", property="file", label="Select File:")# - #submitTag("Upload File")# -#endFormTag()# - - - -function uploadFile() { - if (StructKeyExists(params.document, "file") && IsStruct(params.document.file)) { - // Define upload directory - uploadDir = ExpandPath("files/uploads/documents/"); - - // Ensure directory exists - if (!DirectoryExists(uploadDir)) { - DirectoryCreate(uploadDir); - } - - // Upload file - fileUpload(destination=uploadDir, nameConflict="makeUnique"); - - flashInsert(success="File uploaded successfully!"); - } else { - flashInsert(error="Please select a file to upload."); - } - - redirectTo(action="index"); -} -``` - -## Asset Optimization and CDN - -### Asset Configuration - -**Global Asset Settings** (in `/config/settings.cfm`): -```cfm - -// Enable asset query strings for cache busting -set(assetQueryString = true); - -// CDN configuration -set(assetPaths = { - http: "assets1.example.com,assets2.example.com", - https: "secure-assets.example.com" -}); - -``` - -**Environment-Specific Settings:** -```cfm - -set(assetQueryString = false); - - -set(assetQueryString = true); -set(assetPaths = { - http: "cdn.myapp.com", - https: "cdn.myapp.com" -}); -``` - -### Asset Helper Features - -**Cache Busting:** -```cfm - - - - - -``` - -**CDN Integration:** -```cfm - - - - - -``` - -**Asset Bundling:** -```cfm - -#styleSheetLinkTag("reset,typography,layout,application")# - - - -``` - -## Special Directories - -### Miscellaneous Directory - -The `/public/miscellaneous/` directory is unique - it contains code that runs completely outside the Wheels framework. - -**Structure:** -``` -miscellaneous/ -├── Application.cfc (Empty - bypasses Wheels) -├── flash-gateway.cfm (Flash AMF gateway) -├── ajax-proxy.cfc (AJAX proxy component) -├── legacy-api/ (Legacy API endpoints) -└── webhooks/ (External webhook handlers) -``` - -**Use Cases:** -- Flash AMF bindings that conflict with Wheels -- `` CFC connections -- Legacy code integration -- External API endpoints that need direct access -- Webhook handlers that must bypass framework routing - -**Example Miscellaneous File:** -```cfm - - - - - -// Process webhook payload -payload = GetHttpRequestData(); - -// Handle webhook logic without Wheels -response = { - "status": "success", - "message": "Webhook processed" -}; - -WriteOutput(SerializeJSON(response)); - -``` - -## Security Considerations - -### File Access Control - -**Secure File Storage:** -``` -app/ (NOT web accessible) -├── secure-files/ (Private files) -├── user-data/ (User data storage) -└── temp/ (Temporary files) - -public/ (Web accessible) -├── files/ (Public files only) -└── images/ (Public images only) -``` - -**Controlled File Access:** -```cfm - -function downloadSecureFile() { - // Check user permissions - if (!current.user.hasPermission("download_files")) { - renderText("Access denied"); - return; - } - - // File stored outside web root - secureFilePath = "/app/secure-files/document-" & params.id & ".pdf"; - - if (FileExists(ExpandPath(secureFilePath))) { - sendFile(file=secureFilePath, name="Document.pdf"); - } else { - renderText("File not found"); - } -} -``` - -### Upload Security - -**File Upload Validation:** -```cfm -function uploadFile() { - if (StructKeyExists(params, "uploadedFile")) { - upload = params.uploadedFile; - - // Validate file type - allowedTypes = "jpg,jpeg,png,gif,pdf,doc,docx"; - fileExtension = ListLast(upload.serverFile, "."); - - if (!ListFindNoCase(allowedTypes, fileExtension)) { - flashInsert(error="File type not allowed"); - redirectTo(action="uploadForm"); - return; - } - - // Validate file size (5MB limit) - if (upload.fileSize > 5242880) { - flashInsert(error="File too large"); - redirectTo(action="uploadForm"); - return; - } - - // Store in secure location with sanitized name - safeName = REReplace(upload.clientFile, "[^a-zA-Z0-9\.\-_]", "", "all"); - finalPath = "files/uploads/" & DateFormat(Now(), "yyyy/mm/dd") & "/" & safeName; - - // Move uploaded file - FileMove(upload.serverDirectory & "/" & upload.serverFile, ExpandPath(finalPath)); - - flashInsert(success="File uploaded successfully"); - } - - redirectTo(action="index"); -} -``` - -## Performance Optimization - -### Asset Minification - -**Development Assets:** -``` -stylesheets/ -├── src/ (Source files) -│ ├── base.css -│ ├── components.css -│ └── layout.css -└── application.css (Combined for development) -``` - -**Production Assets:** -``` -stylesheets/ -├── application.min.css (Minified and compressed) -└── vendor.min.css (Third-party libraries) -``` - -**Build Process Integration:** -```json -// package.json -{ - "scripts": { - "build-css": "cat stylesheets/src/*.css > stylesheets/application.css", - "minify-css": "cleancss -o stylesheets/application.min.css stylesheets/application.css", - "build-js": "cat javascripts/src/*.js > javascripts/application.js", - "minify-js": "uglifyjs javascripts/application.js -o javascripts/application.min.js" - } -} -``` - -### Image Optimization - -**Image Processing:** -```cfm - -function processImageUpload() { - if (StructKeyExists(params, "imageFile")) { - // Create optimized versions - originalPath = "images/uploads/original/" & params.imageFile.serverFile; - thumbPath = "images/uploads/thumbs/" & params.imageFile.serverFile; - mediumPath = "images/uploads/medium/" & params.imageFile.serverFile; - - // Resize for different uses - imageResize(source=originalPath, destination=thumbPath, width=150, height=150); - imageResize(source=originalPath, destination=mediumPath, width=400, height=300); - - // Compress original - imageWrite(imageRead(originalPath), originalPath, 0.8); - } -} -``` - -### Caching Strategies - -**Static Asset Caching:** -```cfm - - - -``` - -**Asset Versioning:** -```cfm - -function assetVersion(required string asset) { - filePath = ExpandPath("/" & arguments.asset); - if (FileExists(filePath)) { - fileInfo = GetFileInfo(filePath); - return "?v=" & Hash(fileInfo.lastModified); - } - return ""; -} - -// Usage in views -#styleSheetLinkTag("application" & assetVersion("stylesheets/application.css"))# -``` - -## Development Workflow - -### Local Development Setup - -**Asset Organization:** -``` -public/ -├── stylesheets/ -│ ├── src/ (Source SCSS/LESS files) -│ ├── compiled/ (Compiled CSS) -│ └── vendor/ (Third-party CSS) -├── javascripts/ -│ ├── src/ (Source JS/TypeScript) -│ ├── compiled/ (Compiled JS) -│ └── vendor/ (Third-party JS) -└── images/ - ├── src/ (Original high-res images) - └── optimized/ (Web-optimized images) -``` - -**Build Tools Integration:** -```bash -# Watch for changes during development -npm run watch - -# Build for production -npm run build - -# Optimize images -npm run optimize-images - -# Deploy assets to CDN -npm run deploy-assets -``` - -### Asset Deployment - -**Deployment Strategy:** -1. Build and minify assets locally or in CI/CD -2. Upload to CDN or static file server -3. Update asset configuration with new CDN URLs -4. Deploy application with new asset references - -**CDN Deployment Script:** -```bash -#!/bin/bash -# Build assets -npm run build - -# Upload to CDN -aws s3 sync public/stylesheets/ s3://myapp-assets/stylesheets/ --exclude "*.scss" --exclude "src/*" -aws s3 sync public/javascripts/ s3://myapp-assets/javascripts/ --exclude "*.ts" --exclude "src/*" -aws s3 sync public/images/ s3://myapp-assets/images/ - -# Invalidate CDN cache -aws cloudfront create-invalidation --distribution-id E123456789 --paths "/*" -``` - -## Testing Assets - -### Asset Testing - -**Test Asset Loading:** -```cfm - -component extends="BaseSpec" { - function run() { - describe("Asset Helpers", function() { - it("should generate correct CSS link", function() { - result = styleSheetLinkTag("application"); - expect(result).toInclude('href="/stylesheets/application.css"'); - expect(result).toInclude('rel="stylesheet"'); - }); - - it("should generate correct JS script", function() { - result = javaScriptIncludeTag("application"); - expect(result).toInclude('src="/javascripts/application.js"'); - }); - - it("should generate correct image tag", function() { - // Assuming test image exists - result = imageTag("test.png"); - expect(result).toInclude('src="/images/test.png"'); - expect(result).toInclude('alt="Test"'); - }); - }); - } -} -``` - -**Asset Existence Testing:** -```cfm - -function testCriticalAssets() { - criticalAssets = [ - "stylesheets/application.css", - "javascripts/application.js", - "images/logo.png", - "files/downloads/user-manual.pdf" - ]; - - for (asset in criticalAssets) { - filePath = ExpandPath("/" & asset); - if (!FileExists(filePath)) { - throw(message="Critical asset missing: " & asset); - } - } -} -``` - -## Common Patterns - -### Asset Organization Patterns - -**Component-Based Organization:** -``` -stylesheets/ -├── base/ (Base styles) -│ ├── reset.css -│ ├── typography.css -│ └── grid.css -├── components/ (Reusable components) -│ ├── buttons.css -│ ├── forms.css -│ ├── navigation.css -│ └── tables.css -├── pages/ (Page-specific styles) -│ ├── home.css -│ ├── about.css -│ └── contact.css -└── vendor/ (Third-party) - ├── bootstrap.min.css - └── fontawesome.css -``` - -**Feature-Based Organization:** -``` -javascripts/ -├── core/ (Core functionality) -│ ├── app.js -│ ├── ajax.js -│ └── utils.js -├── features/ (Feature modules) -│ ├── user-management.js -│ ├── product-catalog.js -│ └── shopping-cart.js -├── pages/ (Page controllers) -│ ├── users.js -│ └── products.js -└── vendor/ (Libraries) - ├── jquery.min.js - └── bootstrap.min.js -``` - -### Asset Loading Patterns - -**Progressive Loading:** -```cfm - - - - - -``` - -**Conditional Loading:** -```cfm - - - - #styleSheetLinkTag("base,components,layout,theme")# - #javaScriptIncludeTag("vendor/jquery,vendor/bootstrap,app")# - - - #styleSheetLinkTag("application.min")# - #javaScriptIncludeTag("application.min")# - - - - - #styleSheetLinkTag("admin")# - #javaScriptIncludeTag("admin")# - -``` - -The `/public` directory is the foundation of your Wheels application's web presence, providing secure asset management, framework bootstrap functionality, and flexible file organization patterns that scale from development through production deployment. \ No newline at end of file diff --git a/examples/tweet/tests/CLAUDE.md b/examples/tweet/tests/CLAUDE.md deleted file mode 100755 index 5e4c3f77a9..0000000000 --- a/examples/tweet/tests/CLAUDE.md +++ /dev/null @@ -1,1362 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with tests in a Wheels application. - -## Testing Framework Overview - -Wheels 3.0 uses **TestBox 5** as its primary testing framework, providing modern BDD (Behavior Driven Development) and TDD (Test Driven Development) capabilities. TestBox is automatically included through the `box.json` dependency management. - -### Key TestBox Features -- BDD style (`describe()`, `it()`, `expect()`) and xUnit style testing -- Comprehensive assertion library with fluent syntax -- MockBox integration for mocking and stubbing -- Multiple output formats (HTML, JSON, XML, JUnit, TAP) -- Test lifecycle methods (`beforeAll()`, `beforeEach()`, `afterEach()`, `afterAll()`) -- Asynchronous testing support -- Code coverage reporting via FusionReactor - -## Test Directory Structure - -``` -tests/ -├── runner.cfm # Web-based test runner -├── populate.cfm # Test database setup -├── routes.cfm # Test-specific routes -├── _assets/ # Test assets and helpers -├── specs/ # Test specifications -│ ├── unit/ # Unit tests -│ │ ├── models/ # Model tests -│ │ ├── controllers/ # Controller tests -│ │ └── services/ # Service tests -│ ├── integration/ # Integration tests -│ └── functions/ # Function/helper tests -├── fixtures/ # Test data files -└── support/ # Test utilities and factories -``` - -### File Naming Conventions -- Model tests: `UserTest.cfc` or `UserSpec.cfc` -- Controller tests: `UsersControllerTest.cfc` or `UsersControllerSpec.cfc` -- Integration tests: `UserRegistrationFlowTest.cfc` -- Function tests: `StringHelpersTest.cfc` - -## Writing Tests - -### Basic Test Structure - -All test components extend `wheels.Testbox`: - -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - // Setup once for all tests in this component - variables.testData = { - email: "test@example.com", - firstName: "Test", - lastName: "User" - }; - } - - function afterAll() { - // Cleanup once after all tests complete - model("User").deleteAll(where = "email LIKE '%@example.com'"); - } - - function run() { - describe("User Model", function() { - - it("should validate email presence", function() { - var user = model("User").new(); - user.email = ""; - expect(user.valid()).toBeFalse(); - expect(user.errors).toHaveKey("email"); - }); - - it("should create valid user", function() { - var user = model("User").new(variables.testData); - expect(user.valid()).toBeTrue(); - }); - - }); - } -} -``` - -### Model Testing Patterns - -Focus on validations, associations, callbacks, scopes, and custom methods: - -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - // Setup test data once for all tests in this component - variables.testCategory = model("Category").create(name = "Test Category"); - variables.validProductData = { - name = "Test Product", - price = 19.99, - categoryId = variables.testCategory.id - }; - } - - function afterAll() { - // Cleanup once after all tests complete - model("Product").deleteAll(where = "name LIKE 'Test%'"); - if (isDefined("variables.testCategory")) variables.testCategory.delete(); - } - - function run() { - describe("Product Model", function() { - - describe("Validations", function() { - - it("should require a name", function() { - var product = model("Product").new(); - expect(product.valid()).toBeFalse(); - expect(product.errors).toHaveKey("name"); - }); - - it("should require positive price", function() { - var product = model("Product").new( - name = "Test Product", - price = -10 - ); - expect(product.valid()).toBeFalse(); - expect(product.errors.price).toInclude("greater than 0"); - }); - - it("should validate email format", function() { - var user = model("User").new(email = "invalid-email"); - expect(user.valid()).toBeFalse(); - expect(user.errors.email).toInclude("valid email"); - }); - - }); - - describe("Associations", function() { - - it("should have many reviews", function() { - var product = model("Product").findOne(); - expect(product).toHaveKey("reviews"); - expect(product.reviews()).toBeQuery(); - }); - - it("should belong to category", function() { - var product = createProduct(categoryId = 1); - expect(product.category()).toBeObject(); - expect(product.category().id).toBe(1); - }); - - }); - - describe("Scopes", function() { - - it("should filter active products", function() { - // Create test data - model("Product").create(name = "Active", active = true); - model("Product").create(name = "Inactive", active = false); - - var activeProducts = model("Product").active().findAll(); - expect(activeProducts.recordCount).toBe(1); - expect(activeProducts.name).toBe("Active"); - }); - - }); - - describe("Custom Methods", function() { - - it("should calculate discount price", function() { - var product = model("Product").new(price = 100); - var discountPrice = product.calculateDiscountPrice(0.20); - expect(discountPrice).toBe(80); - }); - - it("should generate slug from name", function() { - var product = model("Product").new(name = "Test Product Name"); - expect(product.generateSlug()).toBe("test-product-name"); - }); - - }); - - }); - } -} -``` - -### Controller Testing Patterns - -Test actions, responses, parameters, authentication, and redirects: - -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - variables.baseUrl = "http://localhost:8080"; - } - - function run() { - describe("Products Controller", function() { - - describe("index action", function() { - - it("should return 200 status for products listing", function() { - cfhttp(url = "#variables.baseUrl#/products", method = "GET", result = "response"); - - expect(response.status_code).toBe(200); - expect(response.filecontent).toInclude("Products"); - }); - - it("should display all products", function() { - // Create test products - var product1 = createProduct(name = "Product 1"); - var product2 = createProduct(name = "Product 2"); - - cfhttp(url = "#variables.baseUrl#/products", method = "GET", result = "response"); - - expect(response.filecontent).toInclude("Product 1"); - expect(response.filecontent).toInclude("Product 2"); - }); - - }); - - describe("create action", function() { - - it("should create product with valid data", function() { - var productData = { - name = "New Product", - price = 29.99, - description = "Test description" - }; - - cfhttp(url = "#variables.baseUrl#/products", method = "POST", result = "response") { - cfhttpparam(type = "formfield", name = "product[name]", value = productData.name); - cfhttpparam(type = "formfield", name = "product[price]", value = productData.price); - cfhttpparam(type = "formfield", name = "product[description]", value = productData.description); - } - - expect(response.status_code).toBe(302); // Redirect after creation - - // Verify product was created - var product = model("Product").findOne(where = "name = 'New Product'"); - expect(product).toBeObject(); - expect(product.price).toBe(29.99); - }); - - it("should reject invalid data", function() { - cfhttp(url = "#variables.baseUrl#/products", method = "POST", result = "response") { - cfhttpparam(type = "formfield", name = "product[name]", value = ""); - cfhttpparam(type = "formfield", name = "product[price]", value = "-10"); - } - - expect(response.status_code).toBe(200); // Returns to form with errors - expect(response.filecontent).toInclude("error"); - }); - - }); - - describe("authentication", function() { - - it("should require authentication for protected actions", function() { - cfhttp(url = "#variables.baseUrl#/products/new", method = "GET", result = "response"); - - expect(response.status_code).toBe(302); - expect(response.responseheader).toHaveKey("Location"); - expect(response.responseheader.Location).toInclude("login"); - }); - - }); - - }); - } -} -``` - -### API Testing Patterns - -Test JSON responses, status codes, and API-specific functionality: - -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - variables.apiUrl = "http://localhost:8080/api/v1"; - } - - function run() { - describe("API Endpoints", function() { - - describe("GET /api/v1/users", function() { - - it("should return JSON with user list", function() { - cfhttp(url = "#variables.apiUrl#/users", method = "GET", result = "response") { - cfhttpparam(type = "header", name = "Accept", value = "application/json"); - } - - expect(response.status_code).toBe(200); - expect(response.responseheader["Content-Type"]).toInclude("application/json"); - - var data = deserializeJSON(response.filecontent); - expect(data).toHaveKey("data"); - expect(data.data).toBeArray(); - }); - - }); - - describe("POST /api/v1/users", function() { - - it("should create user with valid JSON data", function() { - var userData = { - email = "api-test@example.com", - firstName = "API", - lastName = "Test" - }; - - cfhttp(url = "#variables.apiUrl#/users", method = "POST", result = "response") { - cfhttpparam(type = "header", name = "Content-Type", value = "application/json"); - cfhttpparam(type = "body", value = serializeJSON(userData)); - } - - expect(response.status_code).toBe(201); - - var responseData = deserializeJSON(response.filecontent); - expect(responseData.data.email).toBe(userData.email); - }); - - }); - - }); - } -} -``` - -### Integration Testing Patterns - -Test complete workflows and user interactions: - -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - variables.baseUrl = "http://localhost:8080"; - } - - function run() { - describe("User Registration Flow", function() { - - it("should allow complete user registration process", function() { - // 1. Visit registration page - cfhttp(url = "#variables.baseUrl#/register", method = "GET", result = "response"); - expect(response.status_code).toBe(200); - expect(response.filecontent).toInclude("Register"); - - // 2. Submit registration form - var userData = { - email = "integration-test@example.com", - password = "SecurePass123!", - firstName = "Integration", - lastName = "Test" - }; - - cfhttp(url = "#variables.baseUrl#/users", method = "POST", result = "response") { - cfhttpparam(type = "formfield", name = "user[email]", value = userData.email); - cfhttpparam(type = "formfield", name = "user[password]", value = userData.password); - cfhttpparam(type = "formfield", name = "user[firstName]", value = userData.firstName); - cfhttpparam(type = "formfield", name = "user[lastName]", value = userData.lastName); - } - - // 3. Should redirect after successful registration - expect(response.status_code).toBe(302); - - // 4. Verify user was created - var user = model("User").findOne(where = "email = '#userData.email#'"); - expect(user).toBeObject(); - expect(user.firstName).toBe(userData.firstName); - - // 5. Should be able to login with new credentials - cfhttp(url = "#variables.baseUrl#/login", method = "POST", result = "loginResponse") { - cfhttpparam(type = "formfield", name = "email", value = userData.email); - cfhttpparam(type = "formfield", name = "password", value = userData.password); - } - - expect(loginResponse.status_code).toBe(302); // Successful login redirect - }); - - }); - } -} -``` - -### Function/Helper Testing - -Test utility functions and global helpers: - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("String Helper Functions", function() { - - it("should format currency correctly", function() { - expect(formatCurrency(19.99)).toBe("$19.99"); - expect(formatCurrency(1000)).toBe("$1,000.00"); - expect(formatCurrency(0)).toBe("$0.00"); - expect(formatCurrency(-50.50)).toBe("-$50.50"); - }); - - it("should truncate text properly", function() { - var longText = "This is a very long text that needs truncation"; - expect(truncateText(longText, 20)).toBe("This is a very long..."); - expect(truncateText("Short", 20)).toBe("Short"); - }); - - it("should validate email addresses", function() { - expect(isValidEmail("test@example.com")).toBeTrue(); - expect(isValidEmail("user+tag@domain.co.uk")).toBeTrue(); - expect(isValidEmail("invalid-email")).toBeFalse(); - expect(isValidEmail("")).toBeFalse(); - }); - - }); - - describe("Date Helper Functions", function() { - - it("should format dates for display", function() { - var testDate = createDate(2024, 3, 15); - expect(formatDisplayDate(testDate)).toInclude("Mar"); - expect(formatDisplayDate(testDate)).toInclude("15"); - expect(formatDisplayDate(testDate)).toInclude("2024"); - }); - - it("should calculate time differences", function() { - var startTime = now(); - var endTime = dateAdd("h", 2, startTime); - expect(hoursBetween(startTime, endTime)).toBe(2); - }); - - }); - } -} -``` - -## Test Helpers and Utilities - -### Creating Test Data Factories - -Create reusable factory functions for consistent test data: - -```cfm -// In /tests/support/Factories.cfc -component { - - function createUser(struct overrides = {}) { - var defaults = { - email = "user-#createUUID()#@test.com", - password = "password123", - firstName = "Test", - lastName = "User", - isActive = true - }; - - defaults.append(arguments.overrides); - return model("User").create(defaults); - } - - function createProduct(struct overrides = {}) { - var defaults = { - name = "Product #createUUID()#", - price = randRange(10, 100) + (randRange(0, 99) / 100), - description = "Test product description", - isActive = true - }; - - defaults.append(arguments.overrides); - return model("Product").create(defaults); - } - - function buildUser(struct overrides = {}) { - // Build without saving to database - var defaults = createUserDefaults(); - defaults.append(arguments.overrides); - return model("User").new(defaults); - } - -} -``` - -### Custom Assertion Helpers - -Create domain-specific assertions for cleaner tests: - -```cfm -// In your test file -function assertValidUser(required any user) { - expect(arguments.user).toBeObject("User should be an object"); - expect(arguments.user.id).toBeGT(0, "User should have a valid ID"); - expect(arguments.user.email).toMatch(".*@.*\\..*", "User should have valid email"); - expect(arguments.user.firstName).notToBeEmpty("User should have first name"); -} - -function assertHasErrors(required any model, required string property) { - expect(arguments.model.valid()).toBeFalse("#arguments.property# should have validation errors"); - expect(arguments.model.errors).toHaveKey(arguments.property); -} - -function assertRedirectsTo(required any response, required string expectedUrl) { - expect(arguments.response.status_code).toBe(302, "Should redirect"); - expect(arguments.response.responseheader).toHaveKey("Location"); - expect(arguments.response.responseheader.Location).toInclude(arguments.expectedUrl); -} -``` - -### Test Database Management - -Manage test data with transactions or cleanup methods: - -```cfm -// Using transactions (automatic rollback) - Valid use of beforeEach/afterEach -// This is one of the few cases where beforeEach/afterEach are appropriate -describe("Product Model", function() { - - beforeEach(function() { - transaction action="begin"; - }); - - afterEach(function() { - transaction action="rollback"; - }); - - it("should create product", function() { - var product = createProduct(); - expect(product.id).toBeGT(0); - // Automatically rolled back after test - }); - -}); - -// Using manual cleanup - AVOID if possible, slower performance -describe("User Registration", function() { - - afterEach(function() { - // AVOID: This runs after every single test, slowing down the suite - model("User").deleteAll(where = "email LIKE '%@test.com'"); - }); - - // Multiple test methods here... - -}); - -// PREFERRED: Performance-optimized approach -// /tests/specs/models/ProductModelTest.cfc -component extends="wheels.Testbox" { - - function beforeAll() { - // Expensive setup once per component - much faster - variables.testCategory = model("Category").create(name = "Test Category"); - variables.testUser = model("User").create(email = "test@example.com"); - } - - function afterAll() { - // Clean up once per component - model("Product").deleteAll(where = "name LIKE 'Test%'"); - if (isDefined("variables.testCategory")) variables.testCategory.delete(); - if (isDefined("variables.testUser")) variables.testUser.delete(); - } - - function run() { - describe("Product Model Tests", function() { - - // Tests can use shared test data - much faster than recreating for each test - it("should create product with category", function() { - var product = model("Product").create( - name = "Test Product", - categoryId = variables.testCategory.id - ); - expect(product.categoryId).toBe(variables.testCategory.id); - }); - - it("should create product with user", function() { - var product = model("Product").create( - name = "Test User Product", - userId = variables.testUser.id - ); - expect(product.userId).toBe(variables.testUser.id); - }); - - }); - } -} -``` - -## Running Tests - -### Web Interface - -Access tests through the browser: -``` -# Run all tests -http://localhost:8080/tests/runner.cfm - -# JSON output for CI/CD -http://localhost:8080/tests/runner.cfm?format=json - -# Text output -http://localhost:8080/tests/runner.cfm?format=txt - -# JUnit XML output -http://localhost:8080/tests/runner.cfm?format=junit -``` - -### Wheels CLI Commands - -Use Wheels-specific test commands: - -```bash -# Run all tests -wheels test run -#Available Flags ---coverage (Generate coverage report (boolean flag)) --norecurse (Not recurse into subdirectories) ---failFast (Stop on first test failure (boolean flag)) --noverbose (Not verbose output) ---nocoverage (Not generate coverage report (boolean flag)) --recurse (Recurse into subdirectories) ---nofailFast (Not stop on first test failure (boolean flag)) --verbose (Verbose output) -# Available Parameters -bundles= group= (Run specific test group) -coverage= (Generate coverage report (boolean flag)) recurse= (Recurse into subdirectories) -directory= reporter= (Test reporter format (text, json, junit, tap, antjunit)) -failFast= (Stop on first test failure (boolean flag)) servername= (Name of server to use) -filter= (Filter tests by pattern or name) type= (Type of tests to run: (app, core)) -format= verbose= (Verbose output) - -# Generate test files -wheels generate test model User -wheels generate test controller Products --crud - -``` - -## Test Configuration - -### Test Environment Setup - -Configure test-specific settings in `/tests/populate.cfm`: - -```cfm - - // Set test environment variables - application.wheels.dataSourceName = "myapp_test"; - application.wheels.environment = "testing"; - - // Disable certain features during testing - application.wheels.sendEmailOnError = false; - application.wheels.cachePages = false; - - // Create test data - try { - // Create test users - var testUser = model("User").findOne(where = "email = 'test@example.com'"); - if (!isObject(testUser)) { - testUser = model("User").create({ - email = "test@example.com", - password = "password123", - firstName = "Test", - lastName = "User" - }); - } - - // Create test categories - var testCategory = model("Category").findOne(where = "name = 'Test Category'"); - if (!isObject(testCategory)) { - testCategory = model("Category").create({ - name = "Test Category", - description = "Category for testing" - }); - } - - } catch (any e) { - writeOutput("Error setting up test data: #e.message#
    "); - } -
    -``` - -### Database Configuration for Testing - -Use a separate test database: - -```cfm -// In /config/testing/settings.cfm - - set(dataSourceName = "myapp_test"); - set(environment = "testing"); - set(showErrorInformation = true); - set(sendEmailOnError = false); - set(cachePages = false); - set(cacheQueries = false); - set(transactionMode = "rollback"); // Auto-rollback transactions - -``` - -## Best Practices - -### Test Organization -1. **Group related tests** in `describe()` blocks by feature or method -2. **Use nested describe blocks** for complex scenarios -3. **Keep test files focused** on single components -4. **Name tests descriptively** - explain the expected behavior - -### Test Data Management -1. **Use factories** for consistent test data creation -2. **Clean up after tests** using transactions or explicit cleanup -3. **Avoid dependencies** between tests - each should be independent -4. **Use fixtures** for complex, shared test scenarios - -### Test Writing Guidelines -1. **Follow AAA pattern**: Arrange, Act, Assert -2. **Test one thing per test** - single responsibility -3. **Use meaningful assertions** with custom error messages -4. **Test edge cases** - empty data, null values, boundary conditions -5. **Mock external dependencies** - APIs, file systems, email services - -### Performance Considerations -1. **Keep tests fast** - optimize slow database operations -2. **Use in-memory databases** for unit tests when possible -3. **Minimize I/O operations** in test setup -4. **Run unit tests frequently**, integration tests less often -5. **Use lifecycle methods efficiently**: - - **Prefer `beforeAll()` and `afterAll()`** - Run once per test file for expensive setup/teardown - - **Use `beforeEach()` and `afterEach()` sparingly** - These run before/after every test and can significantly slow down test suites - - **Only use `beforeEach()`/`afterEach()` when tests need isolated state** - For example, when tests modify shared data - -### Continuous Integration -1. **Run tests on every commit** using pre-commit hooks -2. **Use different test databases** for different environments -3. **Generate test reports** for CI/CD pipelines -4. **Monitor code coverage** trends over time - -## Common Test Patterns - -### Testing Validations -```cfm -it("should require unique email addresses", function() { - var existingUser = createUser(email = "test@example.com"); - var duplicateUser = buildUser(email = "test@example.com"); - - expect(duplicateUser.valid()).toBeFalse(); - assertHasErrors(duplicateUser, "email"); -}); -``` - -### Testing Callbacks -```cfm -it("should hash password before saving", function() { - var user = model("User").new(password = "plaintext"); - user.save(); - - expect(user.passwordHash).notToBe("plaintext"); - expect(len(user.passwordHash)).toBeGT(20); -}); -``` - -### Testing Scopes -```cfm -it("should find only published posts", function() { - createPost(title = "Published", published = true); - createPost(title = "Draft", published = false); - - var publishedPosts = model("Post").published().findAll(); - expect(publishedPosts.recordCount).toBe(1); - expect(publishedPosts.title).toBe("Published"); -}); -``` - -### Testing Error Handling -```cfm -it("should handle missing records gracefully", function() { - expect(function() { - var post = model("Post").findByKey(99999); - if (!isObject(post)) { - throw(message = "Post not found"); - } - }).toThrow(); -}); -``` - -### Testing Associations -```cfm -it("should create associated records", function() { - var user = createUser(); - var post = user.createPost(title = "My Post", content = "Content"); - - expect(post.userId).toBe(user.id); - expect(user.posts().recordCount).toBe(1); -}); -``` - -## Migration from Legacy Tests - -If you have legacy RocketUnit tests, you can migrate them: - -### Syntax Changes -- `assert(expression)` → `expect(result).toBeTrue()` -- `assert(!expression)` → `expect(result).toBeFalse()` -- Test methods → Wrap in `describe()` and `it()` blocks -- `packageSetup()` → `beforeAll()` component-level function -- `setup()` → `beforeEach()` in describe block -- `teardown()` → `afterEach()` in describe block - -### Structure Changes -- Move tests to `/tests/specs/` directory -- Change component extension from `app.tests.Test` to `wheels.Testbox` -- Wrap individual test methods in `it()` blocks within `describe()` blocks - -## Using application.wo in Tests - -The `application.wo` object is the global Wheels framework instance that provides direct access to all framework functionality. Understanding how to use it effectively in tests is crucial for testing framework internals, accessing models directly, and working with the Wheels testing environment. - -### Understanding application.wo - -`application.wo` is created during `onApplicationStart()` in `Application.cfc` and provides: - -- **Framework Access**: All Wheels framework methods and utilities -- **Model Access**: Direct access to models without instantiation -- **Internal Functions**: Configuration, caching, debugging, templating -- **Test Environment**: Special testing methods and state management - -### Basic application.wo Usage in Tests - -#### Accessing Models Directly - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("User Model via application.wo", function() { - - it("should create user using application.wo", function() { - var user = application.wo.model("User").create({ - email = "test@example.com", - firstName = "Test", - lastName = "User" - }); - - expect(user).toBeObject(); - expect(user.email).toBe("test@example.com"); - }); - - it("should access model methods through application.wo", function() { - var userCount = application.wo.model("User").count(); - expect(userCount).toBeGT(0); - - var users = application.wo.model("User").findAll(); - expect(users).toBeQuery(); - }); - - }); - } -} -``` - -#### Framework Configuration Access - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Framework Configuration", function() { - - it("should access framework settings", function() { - var environment = application.wo.get("environment"); - expect(environment).toBe("testing"); - - var dataSource = application.wo.get("dataSourceName"); - expect(dataSource).toInclude("test"); - }); - - it("should modify settings for test", function() { - // Store original value - var originalValue = application.wo.get("sendEmailOnError"); - - // Temporarily change setting - application.wo.set(sendEmailOnError = false); - expect(application.wo.get("sendEmailOnError")).toBeFalse(); - - // Restore original value - application.wo.set(sendEmailOnError = originalValue); - }); - - }); - } -} -``` - -### Advanced Testing Patterns with application.wo - -#### Testing Controllers Through Framework - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Controllers via application.wo", function() { - - it("should access controller directly", function() { - // Create controller instance - var controller = application.wo.controller("Users"); - expect(controller).toBeObject(); - - // Test controller methods - expect(controller).toHaveKey("index"); - expect(controller).toHaveKey("show"); - }); - - it("should test controller with mock params", function() { - var controller = application.wo.controller("Users"); - - // Mock request parameters - controller.params = { - key = 1, - user = { - firstName = "Test", - lastName = "User" - } - }; - - // Test controller action behavior - try { - controller.show(); - expect(controller.user).toBeObject(); - } catch (any e) { - // Handle expected errors in testing - expect(e.type).toBe("Wheels.RecordNotFound"); - } - }); - - }); - } -} -``` - -#### Database and Migration Testing - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Database Operations", function() { - - it("should access database information", function() { - var dbInfo = application.wo.$dbinfo( - datasource = application.wo.get("dataSourceName"), - type = "version" - ); - - expect(dbInfo).toBeStruct(); - expect(dbInfo).toHaveKey("database_version"); - }); - - it("should execute raw SQL", function() { - sql = "SELECT COUNT(*) AS userCount FROM users WHERE email LIKE :email"; - var result = queryExecute(sql, { email = { value = "%@test.com", cfsqltype = "cf_sql_varchar" } }, { datasource = "yourDatasourceName" }); - - expect(result).toBeQuery(); - expect(result.userCount[1]).toBeGTE(0); - }); - - }); - } -} -``` - -### Testing Framework Internals - -#### Template and Include Testing - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Template System", function() { - - it("should include templates through framework", function() { - // Test template inclusion - var templateOutput = application.wo.$includeAndReturnOutput( - template = "/tests/_assets/test_template.cfm" - ); - - expect(templateOutput).toInclude("Test Template"); - }); - - it("should handle missing templates gracefully", function() { - expect(function() { - application.wo.$include(template = "/nonexistent/template.cfm"); - }).toThrow(); - }); - - }); - } -} -``` - -#### Route Testing - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Routing System", function() { - - it("should recognize defined routes", function() { - // Test route recognition - var routeMatch = application.wo.$findMatchingRoute( - path = "/users/123" - ); - - expect(routeMatch).toBeStruct(); - expect(routeMatch.controller).toBe("users"); - expect(routeMatch.action).toBe("show"); - expect(routeMatch.key).toBe("123"); - }); - - it("should generate URLs from routes", function() { - var userUrl = application.wo.urlFor( - controller = "users", - action = "show", - key = 123 - ); - - expect(userUrl).toBe("/users/show/123"); - }); - - }); - } -} -``` - -### Test Environment Management - -#### Test Data Management - -```cfm -component extends="wheels.Testbox" { - - function beforeAll() { - // Store original test environment state - variables.originalTestState = application.wo.$getTestRunnerApplicationScope(); - } - - function afterAll() { - // Restore test environment - if (isDefined("variables.originalTestState")) { - application.wo.$restoreTestRunnerApplicationScope(variables.originalTestState); - } - - // Clean up test data using application.wo - application.wo.model("User").deleteAll(where = "email LIKE '%@test.com'"); - } - - function run() { - describe("Test Environment Management", function() { - - it("should isolate test data", function() { - // Create test data in isolation - var testUser = application.wo.model("User").create({ - email = "isolation-test@test.com", - firstName = "Isolation", - lastName = "Test" - }); - - expect(testUser).toBeObject(); - expect(testUser.persisted()).toBeTrue(); - }); - - }); - } -} -``` - -#### Transaction-Based Testing - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Transaction Testing", function() { - - beforeEach(function() { - // Start transaction for each test - application.wo.model("User").$getConnection().startTransaction(); - }); - - afterEach(function() { - // Rollback after each test - application.wo.model("User").$getConnection().rollbackTransaction(); - }); - - it("should rollback changes automatically", function() { - var initialCount = application.wo.model("User").count(); - - application.wo.model("User").create({ - email = "rollback-test@test.com", - firstName = "Rollback", - lastName = "Test" - }); - - var newCount = application.wo.model("User").count(); - expect(newCount).toBe(initialCount + 1); - - // This will be rolled back in afterEach - }); - - }); - } -} -``` - -### Debugging and Introspection - -#### Framework State Inspection - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Framework Debugging", function() { - - it("should inspect framework configuration", function() { - // Get all framework settings - var settings = application.wo.$getFrameworkSettings(); - - expect(settings).toBeStruct(); - expect(settings).toHaveKey("environment"); - expect(settings).toHaveKey("dataSourceName"); - - // Inspect specific subsystems - expect(settings.environment).toBe("testing"); - }); - - it("should access debug information", function() { - // Enable debugging for test - application.wo.set(showDebugInformation = true); - - // Check debug state - var debugEnabled = application.wo.get("showDebugInformation"); - expect(debugEnabled).toBeTrue(); - }); - - }); - } -} -``` - -### Common application.wo Test Patterns - -#### Model Factory Pattern - -```cfm -// In your test support files -component { - - function createUserViaFramework(struct attributes = {}) { - var defaults = { - email = "user-#createUUID()#@test.com", - firstName = "Test", - lastName = "User" - }; - - defaults.append(attributes); - return application.wo.model("User").create(defaults); - } - - function findOrCreateUser(required string email) { - var user = application.wo.model("User").findOne(where = "email = '#arguments.email#'"); - - if (!isObject(user)) { - user = application.wo.model("User").create({ - email = arguments.email, - firstName = "Generated", - lastName = "User" - }); - } - - return user; - } - -} -``` - -#### Configuration Testing Pattern - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Configuration Testing", function() { - - it("should handle different environments", function() { - var originalEnv = application.wo.get("environment"); - - try { - // Test production-like settings - application.wo.set(environment = "production"); - application.wo.set(showErrorInformation = false); - - expect(application.wo.get("environment")).toBe("production"); - expect(application.wo.get("showErrorInformation")).toBeFalse(); - - } finally { - // Always restore original environment - application.wo.set(environment = originalEnv); - application.wo.set(showErrorInformation = true); - } - }); - - }); - } -} -``` - -### Performance Testing with application.wo - -#### Query Performance Testing - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("Performance Testing", function() { - - it("should measure query performance", function() { - var startTime = getTickCount(); - - var users = application.wo.model("User").findAll( - select = "id,email,firstName,lastName", - limit = 100 - ); - - var endTime = getTickCount(); - var queryTime = endTime - startTime; - - expect(queryTime).toBeLT(1000, "Query should complete in under 1 second"); - expect(users.recordCount).toBeLTE(100); - }); - - it("should test caching effectiveness", function() { - // First query (uncached) - var startTime1 = getTickCount(); - var users1 = application.wo.model("User").findAll(cache = 60); - var queryTime1 = getTickCount() - startTime1; - - // Second query (should be cached) - var startTime2 = getTickCount(); - var users2 = application.wo.model("User").findAll(cache = 60); - var queryTime2 = getTickCount() - startTime2; - - expect(queryTime2).toBeLT(queryTime1, "Cached query should be faster"); - expect(users2.recordCount).toBe(users1.recordCount); - }); - - }); - } -} -``` - -### Troubleshooting application.wo in Tests - -#### Common Issues and Solutions - -**Issue**: `application.wo` is undefined -```cfm -// Solution: Ensure test environment is properly initialized -function beforeAll() { - // Force application start if needed - if (!isDefined("application.wo")) { - new Application().onApplicationStart(); - } -} -``` - -**Issue**: Database connection errors -```cfm -// Solution: Verify test database configuration -function beforeAll() { - expect(application.wo.get("dataSourceName")).toInclude("test"); - - try { - var testQuery = application.wo.model("User").findAll(limit = 1); - expect(testQuery).toBeQuery(); - } catch (any e) { - fail("Database connection failed: #e.message#"); - } -} -``` - -**Issue**: Model not found errors -```cfm -// Solution: Check model loading and paths -function run() { - describe("Model Loading", function() { - it("should load all required models", function() { - var requiredModels = ["User", "Product", "Category"]; - - for (var modelName in requiredModels) { - expect(function() { - var model = application.wo.model(modelName); - expect(model).toBeObject(); - }).notToThrow("Model #modelName# should load without errors"); - } - }); - }); -} -``` - -### Integration with CI/CD - -#### Test Environment Validation - -```cfm -component extends="wheels.Testbox" { - - function run() { - describe("CI/CD Environment", function() { - - it("should validate test environment setup", function() { - // Verify we're in test environment - expect(application.wo.get("environment")).toBe("testing"); - - // Verify test database - var dataSourceName = application.wo.get("dataSourceName"); - expect(dataSourceName).toInclude("test", "Should use test database"); - - // Verify email is disabled - expect(application.wo.get("sendEmailOnError")).toBeFalse(); - }); - - it("should have required test data", function() { - // Verify test data exists - var userCount = application.wo.model("User").count(); - expect(userCount).toBeGTE(1, "Should have at least one test user"); - }); - - }); - } -} -``` - -Using `application.wo` effectively in your tests provides deep access to the Wheels framework, enabling comprehensive testing of both your application code and the framework integration. This approach is particularly valuable for integration testing, framework customization testing, and debugging complex application behaviors. - -## Testing Resources - -### Documentation Links -- [TestBox Documentation](https://testbox.ortusbooks.com/) - Complete TestBox guide -- [MockBox Documentation](https://testbox.ortusbooks.com/v6.x/mockbox) - Mocking framework -- [Wheels Testing Guide](https://wheels.dev/3.0.0/guides/working-with-wheels/testing-your-application) - Framework-specific testing - -### Common Matchers -- `toBe(expected)` - Exact equality -- `toBeTrue()` / `toBeFalse()` - Boolean assertions -- `toBeEmpty()` / `notToBeEmpty()` - Empty/non-empty checks -- `toHaveKey(key)` - Struct/object key existence -- `toInclude(substring)` - String/array contains -- `toBeGT(number)` / `toBeLT(number)` - Numeric comparisons -- `toMatch(regex)` - Regular expression matching -- `toThrow()` - Exception testing - -This comprehensive testing approach ensures your Wheels application is thoroughly validated across all components while maintaining clean, readable, and maintainable test code. \ No newline at end of file diff --git a/templates/base/src/CLAUDE.md b/templates/base/src/CLAUDE.md deleted file mode 100755 index 28c60b7ff5..0000000000 --- a/templates/base/src/CLAUDE.md +++ /dev/null @@ -1,547 +0,0 @@ -# CLAUDE.md - -Guidance for Claude Code when working with Wheels applications. - -## 🚨 MANDATORY: 5-Step Development Workflow - -**AI ASSISTANTS MUST FOLLOW THIS EXACT ORDER:** - -### 🛑 STEP 1: Check MCP Tools -```bash -ls .mcp.json # If exists → MCP tools are MANDATORY -``` -- ✅ Use `mcp__wheels__*` tools for ALL development -- ❌ NEVER use CLI commands (`wheels g`, `wheels test`) - -### 🛑 STEP 2: Verify MCP Connection -```javascript -mcp__wheels__wheels_server(action="status") -``` - -### 🛑 STEP 3: Invoke Claude Code Skills (MANDATORY) - -**🔴 CRITICAL: Invoke appropriate skill BEFORE any code generation or debugging** - -**Available Skills:** -- `wheels-model-generator` - Models, validations, associations -- `wheels-controller-generator` - Controllers, actions, filters -- `wheels-view-generator` - Views, forms, layouts, partials -- `wheels-migration-generator` - Database migrations -- `wheels-routing-generator` - RESTful routes, nested routes -- `wheels-email-generator` - Mailers, email templates -- `wheels-plugin-generator` - Create Wheels plugins -- `wheels-test-generator` - TestBox tests -- `wheels-debugging` - Troubleshoot errors (use during testing!) -- `wheels-auth-generator` - Authentication systems -- `wheels-api-generator` - RESTful APIs -- `wheels-anti-pattern-detector` - Prevent common errors -- `wheels-refactoring` - Optimize code -- `wheels-deployment` - Production configuration -- `wheels-documentation-generator` - Generate documentation - -**When to Invoke:** -- ✅ Before generating ANY Wheels code -- ✅ **Immediately when encountering errors during testing** -- ✅ When creating missing views or fixing bugs -- ✅ When debugging form/validation issues - -### 🛑 STEP 4: Generate Code with MCP Tools -```javascript -// After skill guidance, use MCP tools -mcp__wheels__wheels_generate(type="model", name="Post", attributes="title:string") -mcp__wheels__wheels_migrate(action="latest") -``` - -### 🛑 STEP 5: Browser Testing (MANDATORY) - -**When errors occur during testing:** -1. **STOP immediately** -2. **Invoke appropriate skill** (`wheels-debugging`, `wheels-view-generator`) -3. **Follow skill guidance** for fixes -4. **Apply fixes** using skill patterns -5. **Re-test** to verify - -**Browser Testing Options (in order of preference):** - -**Option A: Puppeteer MCP (Preferred)** -```javascript -// 1. Verify server -mcp__wheels__wheels_server(action="status") - -// 2. Navigate and test -mcp__puppeteer__puppeteer_navigate(url="http://localhost:PORT") -mcp__puppeteer__puppeteer_screenshot(name="test") - -// 3. Test user flows (navigation, forms, CRUD operations) -``` - -**Option B: Playwright MCP** -```javascript -mcp__playwright__playwright_navigate(url="http://localhost:PORT") -mcp__playwright__playwright_screenshot() -``` - -**Option C: Browser MCP (if Puppeteer/Playwright unavailable)** -```javascript -mcp__browsermcp__browser_navigate(url="http://localhost:PORT") -mcp__browsermcp__browser_screenshot() -``` - -**When error occurs during testing:** -```javascript -// 4. INVOKE SKILL FIRST - Get guidance before fixing -Skill("wheels-debugging") -``` - ---- - -## 🔴 Critical Anti-Patterns (Production-Tested) - -### View Anti-Patterns - -#### 1. CSRF Token Conflicts (CRITICAL) -**❌ WRONG - Causes `Wheels.InvalidAuthenticityToken`:** -```cfm -#startFormTag(route="login", method="post")# - -``` -**✅ CORRECT - startFormTag() includes token automatically:** -```cfm -#startFormTag(route="login", method="post")# - -``` - -#### 2. Property Access on New Objects (CRITICAL) -**❌ WRONG - Causes "no accessible Member" error:** -```cfm -value="#post.title#" -``` -**✅ CORRECT - Use structKeyExists():** -```cfm -value="#structKeyExists(post, 'title') ? post.title : ''#" -``` - -For select options: -```cfm -

    6)G%j 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 MFo^%F#D9eUAMA-${{R30 From fbb343e4c9983fd02cc9db55d31b7f4e9311d386 Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Thu, 15 Jan 2026 14:57:58 +0500 Subject: [PATCH 008/405] commit: update wheels windows installer with new changes --- tools/installer/windows/install-wheels.iss | 14 +++++++------- .../windows/installer/wheels-installer.exe | Bin 1938656 -> 1938696 bytes 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/installer/windows/install-wheels.iss b/tools/installer/windows/install-wheels.iss index 8a8e1b5923..4140c069c9 100644 --- a/tools/installer/windows/install-wheels.iss +++ b/tools/installer/windows/install-wheels.iss @@ -145,10 +145,10 @@ begin TemplateRadio1 := TRadioButton.Create(WizardForm); TemplateRadio1.Parent := TemplatesPage.Surface; TemplateRadio1.Left := 8; TemplateRadio1.Top := topPos; TemplateRadio1.Width := TemplatesPage.SurfaceWidth-16; - TemplateRadio1.Caption := '3.0.x - Wheels Base Template - Bleeding Edge'; TemplateRadio1.Checked := True; + TemplateRadio1.Caption := '3.0.x - Wheels Base Template - Stable'; TemplateRadio1.Checked := True; TemplateRadio2 := TRadioButton.Create(WizardForm); TemplateRadio2.Parent := TemplatesPage.Surface; TemplateRadio2.Left := 8; TemplateRadio2.Top := topPos+28; TemplateRadio2.Width := TemplatesPage.SurfaceWidth-16; - TemplateRadio2.Caption := '2.5.x - Wheels Base Template - Stable Release'; + TemplateRadio2.Caption := 'Bleeding Edge - Wheels Base Template'; TemplateRadio3 := TRadioButton.Create(WizardForm); TemplateRadio3.Parent := TemplatesPage.Surface; TemplateRadio3.Left := 8; TemplateRadio3.Top := topPos+56; TemplateRadio3.Width := TemplatesPage.SurfaceWidth-16; TemplateRadio3.Caption := 'Wheels Template - HTMX - Alpine.js - Simple.css'; @@ -239,11 +239,11 @@ end; // --- Template & Engine getters --- function GetTemplate(Param: String): String; begin - if TemplateRadio1.Checked then Result := 'wheels-base-template@BE' - else if TemplateRadio2.Checked then Result := 'wheels-base-template@stable' - else if TemplateRadio3.Checked then Result := 'wheels-htmx-template' - else if TemplateRadio4.Checked then Result := 'wheels-starter-template' - else Result := 'wheels-todomvc-template'; + if TemplateRadio1.Checked then Result := 'wheels-base-template@^3.0.0' + else if TemplateRadio2.Checked then Result := 'wheels-base-template@BE' + else if TemplateRadio3.Checked then Result := 'cfwheels-template-htmx-alpine-simple' + else if TemplateRadio4.Checked then Result := 'wheels-starter-app' + else Result := 'cfwheels-todomvc-htmx'; end; function GetEngine(Param: String): String; diff --git a/tools/installer/windows/installer/wheels-installer.exe b/tools/installer/windows/installer/wheels-installer.exe index 2032d18fbce356714e85c9364676ace7f186ee22..15c7662a2b8ba497f27e709c6b84c0953d2bc2e0 100644 GIT binary patch delta 24570 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