From 8bf3c9ba36dafeb9ecbf666e3e89e166f6b47685 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 12:43:27 +0000 Subject: [PATCH 01/14] Add BoxLang support: .bxmigrations.json config and .bx migration/seed scaffolding Generalizes the CF-prefixed config lookup methods and adds a shared isBoxLangProject() detection (running BoxLang server, or box.json "language": "boxlang") so migrate init, migrate create, and migrate seed create can scaffold BoxLang-flavored files via an explicit --boxlang flag or auto-detection. --- README.md | 23 +++++++--- commands/migrate/create.cfc | 10 +++-- commands/migrate/down.cfc | 2 +- commands/migrate/fresh.cfc | 2 +- commands/migrate/init.cfc | 20 +++++---- commands/migrate/install.cfc | 2 +- commands/migrate/refresh.cfc | 2 +- commands/migrate/reset.cfc | 2 +- commands/migrate/seed/create.cfc | 10 +++-- commands/migrate/seed/run.cfc | 6 +-- commands/migrate/uninstall.cfc | 2 +- commands/migrate/up.cfc | 2 +- models/BaseMigrationCommand.cfc | 73 +++++++++++++++++++++++--------- 13 files changed, 104 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index a711ad7..a8cad2c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ Migrations will still run in v4 using the old configuration structure and locati You can create the new `.cfmigrations.json` config file by running `migrate init`. +> If your project is a [BoxLang](https://boxlang.io) project, the config file will be named +> `.bxmigrations.json` instead. `.bxmigrations.json` is always checked first if both files exist. +> BoxLang detection is automatic (based on a running BoxLang server or a `"language": "boxlang"` +> entry in your `box.json`), or you can force it with `migrate init --boxlang` / `--no-boxlang`. + The new config file format mirrors `CFMigrations`: ```json @@ -209,20 +214,24 @@ You would update your `.gitignore` file to not ignore the `.env.example` file: ## Usage -### `migrate init` +### `migrate init [--boxlang] [--no-boxlang]` -Creates the migration config file as `.cfmigrations.json`, if it doesn't already exist. +Creates the migration config file, if it doesn't already exist. Creates +`.cfmigrations.json` by default, or `.bxmigrations.json` for BoxLang +projects. Pass `--boxlang`/`--no-boxlang` to override auto-detection. ### `migrate install` Installs the migration table in to your database. This migration table keeps track of the ran migrations. -### `migrate create [name]` +### `migrate create [name] [--boxlang] [--no-boxlang]` Creates a migration file with an `up` and `down` method. The file name will be prepended with the current timestamp -in the format that `cfmigrations` expects. +in the format that `cfmigrations` expects. Creates a `.cfc` file by +default, or a `.bx` file for BoxLang projects (auto-detected, or +overridden with `--boxlang`/`--no-boxlang`). ### `migrate up [--once] [--verbose] [--pretend] [file]` @@ -276,9 +285,11 @@ a fresh copy of your migrated database. Removes the `cfmigrations` table after running down any ran migrations. -### `migrate seed create [name]` +### `migrate seed create [name] [--boxlang] [--no-boxlang]` -Creates a new Seeder file. +Creates a new Seeder file. Creates a `.cfc` file by default, or a `.bx` +file for BoxLang projects (auto-detected, or overridden with +`--boxlang`/`--no-boxlang`). ### `migrate seed run` diff --git a/commands/migrate/create.cfc b/commands/migrate/create.cfc index 7f62810..f10b730 100644 --- a/commands/migrate/create.cfc +++ b/commands/migrate/create.cfc @@ -8,12 +8,13 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @name.hint Name of the migration to create without the .cfc. + * @name.hint Name of the migration to create without the extension. * @manager.hint The Migration Manager to use. * @manager.optionsUDF completeManagers * @open.hint Open the file once generated. + * @boxlang.hint Create a .bx file instead of a .cfc. Defaults to auto-detection based on your server/box.json. */ - function run( required string name, string manager = "default", boolean open = false ) { + function run( required string name, string manager = "default", boolean open = false, boolean boxlang ) { setup( manager = arguments.manager, setupDatasource = false ); var migrationsDirectory = expandPath( variables.migrationService.getMigrationsDirectory() ); @@ -23,8 +24,11 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { directoryCreate( migrationsDirectory ); } + var useBoxLang = arguments.keyExists( "boxlang" ) ? arguments.boxlang : isBoxLangProject( getCWD() ); + var extension = useBoxLang ? "bx" : "cfc"; + var timestamp = dateTimeFormat( now(), "yyyy_mm_dd_HHnnss" ); - var migrationPath = "#migrationsDirectory##timestamp#_#arguments.name#.cfc"; + var migrationPath = "#migrationsDirectory##timestamp#_#arguments.name#.#extension#"; var migrationContent = fileRead( "/commandbox-migrations/templates/Migration.txt" ); diff --git a/commands/migrate/down.cfc b/commands/migrate/down.cfc index de14e90..aa187b2 100644 --- a/commands/migrate/down.cfc +++ b/commands/migrate/down.cfc @@ -22,7 +22,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( arguments.verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/commands/migrate/fresh.cfc b/commands/migrate/fresh.cfc index 24fa27b..fdf6569 100644 --- a/commands/migrate/fresh.cfc +++ b/commands/migrate/fresh.cfc @@ -14,7 +14,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( arguments.verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/commands/migrate/init.cfc b/commands/migrate/init.cfc index 4eb8ea1..4b0a7ce 100644 --- a/commands/migrate/init.cfc +++ b/commands/migrate/init.cfc @@ -4,19 +4,21 @@ * * This will ensure the correct values are set in your box.json. */ -component { +component extends="commandbox-migrations.models.BaseMigrationCommand" { - property name="packageService" inject="PackageService"; - property name="JSONService" inject="JSONService"; - - function run( boolean open = false ) { + /** + * @boxlang.hint Create a .bxmigrations.json file instead of .cfmigrations.json. Defaults to auto-detection based on your server/box.json. + */ + function run( boolean open = false, boolean boxlang ) { var directory = getCWD(); - var configPath = "#directory#/.cfmigrations.json"; + var useBoxLang = arguments.keyExists( "boxlang" ) ? arguments.boxlang : isBoxLangProject( directory ); + var configFileName = useBoxLang ? ".bxmigrations.json" : ".cfmigrations.json"; + var configPath = "#directory#/#configFileName#"; - // Check and see if a .cfmigrations.json file exists + // Check and see if the config file already exists if ( fileExists( configPath ) ) { - print.yellowLine( ".cfmigrations.json already exists." ); + print.yellowLine( "#configFileName# already exists." ); return; } @@ -24,7 +26,7 @@ component { file action="write" file="#configPath#" mode="777" output="#trim( configStub )#"; - print.greenLine( "Created .cfmigrations config file." ); + print.greenLine( "Created #configFileName# config file." ); // Open file? if ( arguments.open ) { diff --git a/commands/migrate/install.cfc b/commands/migrate/install.cfc index a30d11c..957073c 100644 --- a/commands/migrate/install.cfc +++ b/commands/migrate/install.cfc @@ -16,7 +16,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } try { diff --git a/commands/migrate/refresh.cfc b/commands/migrate/refresh.cfc index b428364..963d2df 100644 --- a/commands/migrate/refresh.cfc +++ b/commands/migrate/refresh.cfc @@ -14,7 +14,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( arguments.verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/commands/migrate/reset.cfc b/commands/migrate/reset.cfc index 9430ef4..8ae30cb 100644 --- a/commands/migrate/reset.cfc +++ b/commands/migrate/reset.cfc @@ -13,7 +13,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( arguments.verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } try { diff --git a/commands/migrate/seed/create.cfc b/commands/migrate/seed/create.cfc index 5ada685..d0d4282 100644 --- a/commands/migrate/seed/create.cfc +++ b/commands/migrate/seed/create.cfc @@ -5,12 +5,13 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @name.hint Name of the seeder to create without the .cfc. + * @name.hint Name of the seeder to create without the extension. * @manager.hint The Migration Manager to use. * @manager.optionsUDF completeManagers * @open.hint Open the file once generated. + * @boxlang.hint Create a .bx file instead of a .cfc. Defaults to auto-detection based on your server/box.json. */ - function run( required string name, string manager = "default", boolean open = false ) { + function run( required string name, string manager = "default", boolean open = false, boolean boxlang ) { setup( manager = arguments.manager, setupDatasource = false ); var seedsDirectory = expandPath( variables.migrationService.getSeedsDirectory() ); @@ -20,7 +21,10 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { directoryCreate( seedsDirectory ); } - var seedPath = "#seedsDirectory##arguments.name#.cfc"; + var useBoxLang = arguments.keyExists( "boxlang" ) ? arguments.boxlang : isBoxLangProject( getCWD() ); + var extension = useBoxLang ? "bx" : "cfc"; + + var seedPath = "#seedsDirectory##arguments.name#.#extension#"; var seedContent = fileRead( "/commandbox-migrations/templates/seed.txt" ); diff --git a/commands/migrate/seed/run.cfc b/commands/migrate/seed/run.cfc index 8dd60b5..60b39ea 100644 --- a/commands/migrate/seed/run.cfc +++ b/commands/migrate/seed/run.cfc @@ -15,13 +15,13 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { function run( string name = "", string manager = "default", boolean verbose = false ) { setup( arguments.manager ); - if ( getCFMigrationsType() == "boxJSON" ) { + if ( getMigrationsConfigType() == "boxJSON" ) { error( "Seeders can only be ran after migrating to the new v4 migrations configuration." ); } if ( arguments.verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); @@ -71,7 +71,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { function completeSeedNames( string paramSoFar, struct passedNamedParameters ) { param passedNamedParameters.manager = "default"; setup( passedNamedParameters.manager ); - if ( getCFMigrationsType() == "boxJSON" ) { + if ( getMigrationsConfigType() == "boxJSON" ) { return []; } return variables.migrationService.findSeeds() diff --git a/commands/migrate/uninstall.cfc b/commands/migrate/uninstall.cfc index c933f6d..407b989 100644 --- a/commands/migrate/uninstall.cfc +++ b/commands/migrate/uninstall.cfc @@ -17,7 +17,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( arguments.verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/commands/migrate/up.cfc b/commands/migrate/up.cfc index 4105f57..a8fb68c 100644 --- a/commands/migrate/up.cfc +++ b/commands/migrate/up.cfc @@ -24,7 +24,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( arguments.verbose ) { print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/models/BaseMigrationCommand.cfc b/models/BaseMigrationCommand.cfc index 1c52892..94bab22 100644 --- a/models/BaseMigrationCommand.cfc +++ b/models/BaseMigrationCommand.cfc @@ -6,9 +6,10 @@ component { property name="sqlHighlighter" inject="sqlHighlighter"; property name="systemSettings" inject="SystemSettings"; property name="sqlFormatter" inject="Formatter@sqlFormatter"; + property name="serverService" inject="ServerService"; function setup( required string manager, boolean setupDatasource = true ) { - var config = getCFMigrationsInfo(); + var config = getMigrationsInfo(); if ( !config.keyExists( arguments.manager ) ) { error( "No manager found named [#arguments.manager#]. Available managers are: #config.keyList( ", " )#" ); } @@ -71,17 +72,47 @@ component { } } - private struct function getCFMigrationsInfo() { - var cfmigrationsInfoType = "boxJSON"; + private string function findMigrationsConfigPath( required string directory ) { + var candidates = [ ".bxmigrations.json", ".cfmigrations.json" ]; + for ( var candidate in candidates ) { + var path = "#arguments.directory#/#candidate#"; + if ( fileExists( path ) ) { + return path; + } + } + return ""; + } + + private boolean function isBoxLangProject( required string directory ) { + // Detect if the running CommandBox server is BoxLang. + var serverInfo = variables.serverService.resolveServerDetails( {} ).serverInfo; + if ( serverInfo.keyExists( "cfengine" ) && serverInfo.cfengine contains "boxlang" ) { + return true; + } + + // Detect via box.json's language key. + if ( packageService.isPackage( arguments.directory ) ) { + var boxJSON = packageService.readPackageDescriptor( arguments.directory ); + if ( boxJSON.keyExists( "language" ) && boxJSON.language == "boxlang" ) { + return true; + } + } + + return false; + } + + private struct function getMigrationsInfo() { + var migrationsInfoType = "boxJSON"; var directory = getCWD(); - // Check and see if a .cfmigrations.json file exists - if ( fileExists( "#directory#/.cfmigrations.json" ) ) { - var cfmigrationsInfo = deserializeJSON( fileRead( "#directory#/.cfmigrations.json" ) ); - variables.systemSettings.expandDeepSystemSettings( cfmigrationsInfo ); - cfmigrationsInfoType = "cfmigrations"; - return cfmigrationsInfo; + // Check and see if a .bxmigrations.json or .cfmigrations.json file exists + var configPath = findMigrationsConfigPath( directory ); + if ( len( configPath ) ) { + var migrationsInfo = deserializeJSON( fileRead( configPath ) ); + variables.systemSettings.expandDeepSystemSettings( migrationsInfo ); + migrationsInfoType = "cfmigrations"; + return migrationsInfo; } // Check and see if box.json exists @@ -97,10 +128,10 @@ component { var boxJSONMigrationsInfo = JSONService.show( boxJSON, "cfmigrations", {} ); if ( boxJSONMigrationsInfo.keyExists( "managers" ) ) { - var cfmigrationsInfo = boxJSONMigrationsInfo; - variables.systemSettings.expandDeepSystemSettings( cfmigrationsInfo ); - cfmigrationsInfoType = "cfmigrations"; - return cfmigrationsInfo; + var migrationsInfo = boxJSONMigrationsInfo; + variables.systemSettings.expandDeepSystemSettings( migrationsInfo ); + migrationsInfoType = "cfmigrations"; + return migrationsInfo; } print.boldUnderscoredYellowLine( "The format of the migrations configuration has changed in v4." ); @@ -119,7 +150,7 @@ component { properties[ "schema" ] = boxJSONMigrationsInfo.schema; } - var cfmigrationsInfo = { + var migrationsInfo = { "default": { "manager": "cfmigrations.models.QBMigrationManager", "migrationsDirectory": boxJSONMigrationsInfo.migrationsDirectory, @@ -127,15 +158,15 @@ component { } }; - variables.systemSettings.expandDeepSystemSettings( cfmigrationsInfo ); - return cfmigrationsInfo; + variables.systemSettings.expandDeepSystemSettings( migrationsInfo ); + return migrationsInfo; } - private string function getCFMigrationsType() { + private string function getMigrationsConfigType() { var directory = getCWD(); - // Check and see if a .cfmigrations.json file exists - if ( fileExists( "#directory#/.cfmigrations.json" ) ) { + // Check and see if a .bxmigrations.json or .cfmigrations.json file exists + if ( len( findMigrationsConfigPath( directory ) ) ) { return "cfmigrations"; } @@ -155,11 +186,11 @@ component { } function completeManagers( string paramSoFar ) { - var type = getCFMigrationsType(); + var type = getMigrationsConfigType(); if ( type == "boxJSON" ) { return []; } - return getCFMigrationsInfo().keyArray() + return getMigrationsInfo().keyArray() .filter( ( manager ) => startsWith( manager, paramSoFar ) ) .map( ( manager ) => ( { "name": manager, "group": "Managers" } ) ); } From 52388853ce6f5216f75e77c67462cd0b9470ae08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 12:51:13 +0000 Subject: [PATCH 02/14] Document all undocumented methods Adds doc comments to every method in BaseMigrationCommand.cfc, ModuleConfig.cfc's configure(), and seed/run.cfc's completeSeedNames(), the only methods in the module lacking documentation. --- ModuleConfig.cfc | 4 +++ commands/migrate/seed/run.cfc | 4 +++ models/BaseMigrationCommand.cfc | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 1bc51fb..6c3ff6d 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -2,6 +2,10 @@ component { this.dependencies = [ "cfmigrations", "sqlFormatter" ]; + /** + * Module lifecycle method. Registers the SqlHighlighter singleton, falling back + * to a no-op highlighter if the jLine SyntaxHighlighter can't be built. + */ function configure() { var sqlHighlighter = { "highlight": ( str ) => ( { diff --git a/commands/migrate/seed/run.cfc b/commands/migrate/seed/run.cfc index 60b39ea..9c971c3 100644 --- a/commands/migrate/seed/run.cfc +++ b/commands/migrate/seed/run.cfc @@ -68,6 +68,10 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { } } + /** + * Tab-completion options for the `name` argument, listing seed file names for + * the passed manager that start with what's been typed so far. + */ function completeSeedNames( string paramSoFar, struct passedNamedParameters ) { param passedNamedParameters.manager = "default"; setup( passedNamedParameters.manager ); diff --git a/models/BaseMigrationCommand.cfc b/models/BaseMigrationCommand.cfc index 94bab22..5f6adbd 100644 --- a/models/BaseMigrationCommand.cfc +++ b/models/BaseMigrationCommand.cfc @@ -8,6 +8,10 @@ component { property name="sqlFormatter" inject="Formatter@sqlFormatter"; property name="serverService" inject="ServerService"; + /** + * Resolves the named manager's settings from the migrations config and + * initializes `variables.migrationService` from them. + */ function setup( required string manager, boolean setupDatasource = true ) { var config = getMigrationsInfo(); if ( !config.keyExists( arguments.manager ) ) { @@ -43,6 +47,10 @@ component { ); } + /** + * Registers an on-the-fly application datasource from the given connection info + * and sets it as the application default, returning the datasource name. + */ function installDatasource( required struct connectionInfo, string datasourceName = "cfmigrations" ) { var datasources = getApplicationSettings().datasources ?: {}; datasources[ "cfmigrations" ] = arguments.connectionInfo; @@ -51,6 +59,10 @@ component { return arguments.datasourceName; } + /** + * Updates the migration service's migrations directory to the given path, + * resolved and made relative to the current working directory. + */ public void function setMigrationPath( required migrationsDirectory ) { var relativePath = fileSystemUtil.makePathRelative( fileSystemUtil.resolvePath( migrationsDirectory ) @@ -58,10 +70,17 @@ component { migrationService.setMigrationsDirectory( relativePath ); } + /** + * Returns the migration service's currently configured migrations directory. + */ public string function getMigrationPath () { return migrationService.getMigrationsDirectory(); } + /** + * Prompts the user to install the migration table if it hasn't been installed yet, + * aborting the command if they decline. + */ private void function checkForInstalledMigrationTable() { if ( ! variables.migrationService.isReady() ) { if ( confirm( "Migration table not installed. Do you want to install it now? [y\n]" ) ) { @@ -72,6 +91,10 @@ component { } } + /** + * Returns the path to the first migrations config file found in the given directory, + * checking `.bxmigrations.json` before `.cfmigrations.json`, or "" if neither exists. + */ private string function findMigrationsConfigPath( required string directory ) { var candidates = [ ".bxmigrations.json", ".cfmigrations.json" ]; for ( var candidate in candidates ) { @@ -83,6 +106,10 @@ component { return ""; } + /** + * Detects whether the given directory should be treated as a BoxLang project, + * based on the running CommandBox server's engine or box.json's `language` key. + */ private boolean function isBoxLangProject( required string directory ) { // Detect if the running CommandBox server is BoxLang. var serverInfo = variables.serverService.resolveServerDetails( {} ).serverInfo; @@ -101,6 +128,11 @@ component { return false; } + /** + * Loads the migrations config: from `.bxmigrations.json`/`.cfmigrations.json` if present, + * otherwise falls back to the deprecated `cfmigrations` key in box.json (auto-converting + * the legacy pre-v4 format if needed). + */ private struct function getMigrationsInfo() { var migrationsInfoType = "boxJSON"; @@ -162,6 +194,10 @@ component { return migrationsInfo; } + /** + * Returns "cfmigrations" if a dedicated config file or v4-style box.json config is found, + * or "boxJSON" if only the legacy pre-v4 box.json format is present. + */ private string function getMigrationsConfigType() { var directory = getCWD(); @@ -185,6 +221,10 @@ component { return "boxJSON"; } + /** + * Tab-completion options for the `manager` argument, listing configured manager + * names that start with what's been typed so far. + */ function completeManagers( string paramSoFar ) { var type = getMigrationsConfigType(); if ( type == "boxJSON" ) { @@ -195,6 +235,9 @@ component { .map( ( manager ) => ( { "name": manager, "group": "Managers" } ) ); } + /** + * Returns true if `word` starts with `substring` (or `substring` is empty). + */ private string function startsWith( required string word, required string substring ) { if ( len( arguments.substring ) == 0 ) { return true; From a2a5ea87f151b315606361ffcf52c34705520bd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 17:22:19 +0000 Subject: [PATCH 03/14] feat: document BoxLang support in README Adds a dedicated BoxLang Support section covering .bxmigrations.json, .bx file scaffolding, and the auto-detection/--boxlang override behavior, and cross-references it from the Setup section. --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index a8cad2c..6804cb1 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,10 @@ Make sure to append `@qb` to the end of any qb-supplied grammars, like `AutoDisc You need to create a `.cfmigrations.json` config file in your application root folder. You can do this easily by running `migrate init`: +> If your project is a [BoxLang](https://boxlang.io) project, `migrate init` will create +> `.bxmigrations.json` instead, and it will be checked first if both files happen to exist. +> See [BoxLang Support](#boxlang-support) below for details on detection and overrides. + ```json { "default": { @@ -212,6 +216,23 @@ You would update your `.gitignore` file to not ignore the `.env.example` file: !.env.example ``` +## BoxLang Support + +`commandbox-migrations` supports [BoxLang](https://boxlang.io) projects: + +- Config file: `migrate init` writes `.bxmigrations.json` instead of `.cfmigrations.json`. + If both files exist, `.bxmigrations.json` is read first. +- Migration/seed files: `migrate create` and `migrate seed create` scaffold `.bx` files + instead of `.cfc` files. + +Whether a project is treated as BoxLang is auto-detected by checking, in order: + +1. Whether the running CommandBox server's engine is BoxLang. +2. Whether `box.json` has `"language": "boxlang"`. + +You can skip auto-detection and force the behavior on any of `migrate init`, +`migrate create`, and `migrate seed create` with the `--boxlang` / `--no-boxlang` flags. + ## Usage ### `migrate init [--boxlang] [--no-boxlang]` From a90884071c893d4a27eab92763bb4e7832a50ad8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 20:01:09 +0000 Subject: [PATCH 04/14] docs: document all command arguments and flags The Usage section was missing manager, verbose, seed, force, and open flags for most commands even though they were already implemented. Each command's documented signature now matches its run() signature. --- README.md | 86 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 6804cb1..42f1e2e 100644 --- a/README.md +++ b/README.md @@ -235,29 +235,43 @@ You can skip auto-detection and force the behavior on any of `migrate init`, ## Usage -### `migrate init [--boxlang] [--no-boxlang]` +Every command below accepts an optional `manager` argument (defaulting to `default`) +to target a specific named manager from your config file. See +[Upgrading to v4.0.0?](#upgrading-to-v400) above for how to define multiple managers. + +### `migrate init [--open] [--boxlang] [--no-boxlang]` Creates the migration config file, if it doesn't already exist. Creates `.cfmigrations.json` by default, or `.bxmigrations.json` for BoxLang projects. Pass `--boxlang`/`--no-boxlang` to override auto-detection. -### `migrate install` +Passing `--open` opens the config file once it's created. + +### `migrate install [manager] [--verbose]` -Installs the migration table in to your database. +Installs the migration table for the given manager in to your database. This migration table keeps track of the ran migrations. -### `migrate create [name] [--boxlang] [--no-boxlang]` +Passing the `--verbose` flag will show the resolved migrations config +as well as the full stack trace of any errors. + +### `migrate create [name] [manager] [--open] [--boxlang] [--no-boxlang]` -Creates a migration file with an `up` and `down` method. +Creates a migration file with an `up` and `down` method for the given manager. The file name will be prepended with the current timestamp in the format that `cfmigrations` expects. Creates a `.cfc` file by default, or a `.bx` file for BoxLang projects (auto-detected, or overridden with `--boxlang`/`--no-boxlang`). -### `migrate up [--once] [--verbose] [--pretend] [file]` +Passing `--open` opens the migration file once it's created. -Runs all available migrations up. Passing the `--once` flag will only -run a single migration up (if any are available). +### `migrate up [manager] [--seed] [--once] [--verbose] [--pretend] [file]` + +Runs all available migrations up for the given manager. Passing the `--once` +flag will only run a single migration up (if any are available). + +Passing the `--seed` flag will run all seeders for the manager after the +migrations are applied (equivalent to running `migrate seed run` afterward). Passing the `--verbose` flag with show the datasource information passed as well as the full stack trace of any errors. @@ -271,10 +285,10 @@ If provided, the outputted sql will be saved to the file path provided. > **WARNING: `--pretend` only captures SQL from `schema` (SchemaBuilder) and `qb` (QueryBuilder) calls.** > Migrations that use `queryExecute()` directly are **not intercepted** — those queries **will execute against your database** even when `--pretend` is passed. If your migrations use raw `queryExecute()` calls, do not rely on `--pretend` to prevent changes. -### `migrate down [--once] [--verbose] [--pretend] [file]` +### `migrate down [manager] [--once] [--verbose] [--pretend] [file]` -Runs all available migrations down. Passing the `--once` flag will only -run a single migration down (if any are available). +Runs all available migrations down for the given manager. Passing the +`--once` flag will only run a single migration down (if any are available). Passing the `--verbose` flag with show the datasource information passed as well as the full stack trace of any errors. @@ -288,30 +302,48 @@ If provided, the outputted sql will be saved to the file path provided. > **WARNING: `--pretend` only captures SQL from `schema` (SchemaBuilder) and `qb` (QueryBuilder) calls.** > Migrations that use `queryExecute()` directly are **not intercepted** — those queries **will execute against your database** even when `--pretend` is passed. If your migrations use raw `queryExecute()` calls, do not rely on `--pretend` to prevent changes. -### `migrate refresh` +### `migrate refresh [manager] [--seed] [--verbose]` -Runs all available migrations down and then runs all migrations up. +Runs all available migrations down and then runs all migrations up for the +given manager (delegates to `migrate down` and `migrate up`, forwarding +`manager`, `--seed`, and `--verbose`). -### `migrate reset` +### `migrate reset [manager] [--verbose]` -Clears out all objects from the database, including the `cfmigrations` table. -Use this when your database is in an inconsistent state in development. +Clears out all objects from the database, including the `cfmigrations` table, +for the given manager. Use this when your database is in an inconsistent +state in development. -### `migrate fresh` +Passing the `--verbose` flag will show the resolved migrations config +as well as the full stack trace of any errors. -Runs `migrate reset`, `migrate install`, and `migrate up` to give you -a fresh copy of your migrated database. +### `migrate fresh [manager] [--seed] [--verbose]` -### `migrate uninstall` +Runs `migrate reset`, `migrate install`, and `migrate up` (forwarding +`manager`, `--seed`, and `--verbose`) to give you a fresh copy of your +migrated database. -Removes the `cfmigrations` table after running down any ran migrations. +### `migrate uninstall [manager] [--verbose] [--force]` -### `migrate seed create [name] [--boxlang] [--no-boxlang]` +Removes the `cfmigrations` table for the given manager after running down +any ran migrations. Prompts for confirmation before uninstalling unless +`--force` is passed. -Creates a new Seeder file. Creates a `.cfc` file by default, or a `.bx` -file for BoxLang projects (auto-detected, or overridden with -`--boxlang`/`--no-boxlang`). +Passing the `--verbose` flag will show the resolved migrations config +as well as the full stack trace of any errors. -### `migrate seed run` +### `migrate seed create [name] [manager] [--open] [--boxlang] [--no-boxlang]` -Runs one or all Seeders. +Creates a new Seeder file for the given manager. Creates a `.cfc` file by +default, or a `.bx` file for BoxLang projects (auto-detected, or +overridden with `--boxlang`/`--no-boxlang`). + +Passing `--open` opens the seeder file once it's created. + +### `migrate seed run [name] [manager] [--verbose]` + +Runs one or all Seeders for the given manager. Pass `name` to only run a +single named seeder; omit it to run all seeders. + +Passing the `--verbose` flag will show the resolved migrations config +as well as the full stack trace of any errors. From 378834ecbc7a38f3d95e2a772d6de487eedef2fc Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 16 Jun 2026 22:06:07 +0200 Subject: [PATCH 05/14] - add copyrights to all files - remove old .hint that are not required anymore and produce noise --- ModuleConfig.cfc | 7 ++++++- commands/migrate/create.cfc | 18 +++++++++++++----- commands/migrate/down.cfc | 13 ++++++++----- commands/migrate/fresh.cfc | 9 ++++++--- commands/migrate/init.cfc | 5 ++++- commands/migrate/install.cfc | 7 +++++-- commands/migrate/refresh.cfc | 9 ++++++--- commands/migrate/reset.cfc | 4 ++-- commands/migrate/seed/create.cfc | 11 +++++++---- commands/migrate/seed/run.cfc | 9 ++++++--- commands/migrate/uninstall.cfc | 9 ++++++--- commands/migrate/up.cfc | 15 +++++++++------ models/BaseMigrationCommand.cfc | 5 +++++ 13 files changed, 83 insertions(+), 38 deletions(-) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 6c3ff6d..3667f83 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -1,4 +1,9 @@ -component { +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + */ + component { this.dependencies = [ "cfmigrations", "sqlFormatter" ]; diff --git a/commands/migrate/create.cfc b/commands/migrate/create.cfc index f10b730..c4875e7 100644 --- a/commands/migrate/create.cfc +++ b/commands/migrate/create.cfc @@ -1,4 +1,7 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Create a new migration CFC in an existing application. * Make sure you are running this command in the root of your app. * @@ -8,13 +11,18 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @name.hint Name of the migration to create without the extension. - * @manager.hint The Migration Manager to use. + * @name Name of the migration to create without the extension. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @open.hint Open the file once generated. - * @boxlang.hint Create a .bx file instead of a .cfc. Defaults to auto-detection based on your server/box.json. + * @open Open the file once generated. + * @boxlang Create a .bx file instead of a .cfc. Defaults to auto-detection based on your server/box.json. */ - function run( required string name, string manager = "default", boolean open = false, boolean boxlang ) { + function run( + required string name, + string manager = "default", + boolean open = false, + boolean boxlang + ) { setup( manager = arguments.manager, setupDatasource = false ); var migrationsDirectory = expandPath( variables.migrationService.getMigrationsDirectory() ); diff --git a/commands/migrate/down.cfc b/commands/migrate/down.cfc index aa187b2..a26a631 100644 --- a/commands/migrate/down.cfc +++ b/commands/migrate/down.cfc @@ -1,15 +1,18 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Rollback one or all of the migrations already ran against your database. */ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @once.hint Only rollback a single migration. - * @manager.hint The Migration Manager to use. + * @once Only rollback a single migration. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @verbose.hint If true, errors output a full stack trace. - * @pretend.hint If true, only pretends to run the query. The SQL that would have been run is printed to the console. - * @file.hint If provided, outputs the SQL that would have been run to the file. Only applies when running `pretend`. + * @verbose If true, errors output a full stack trace. + * @pretend If true, only pretends to run the query. The SQL that would have been run is printed to the console. + * @file If provided, outputs the SQL that would have been run to the file. Only applies when running `pretend`. */ function run( boolean once = false, diff --git a/commands/migrate/fresh.cfc b/commands/migrate/fresh.cfc index fdf6569..ada2ba7 100644 --- a/commands/migrate/fresh.cfc +++ b/commands/migrate/fresh.cfc @@ -1,13 +1,16 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Resets the database and runs all migrations up. */ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @manager.hint The Migration Manager to use. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @seed.hint If true, runs all seeders for the manager after creating a fresh database. - * @verbose.hint If true, errors output a full stack trace. + * @seed If true, runs all seeders for the manager after creating a fresh database. + * @verbose If true, errors output a full stack trace. */ function run( string manager = "default", boolean seed = false, boolean verbose = false ) { setup( arguments.manager ); diff --git a/commands/migrate/init.cfc b/commands/migrate/init.cfc index 4b0a7ce..45554ed 100644 --- a/commands/migrate/init.cfc +++ b/commands/migrate/init.cfc @@ -1,4 +1,7 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Initialize your project to use commandbox-migrations * Make sure you are running this command in the root of your app. * @@ -7,7 +10,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @boxlang.hint Create a .bxmigrations.json file instead of .cfmigrations.json. Defaults to auto-detection based on your server/box.json. + * @boxlang Create a .bxmigrations.json file instead of .cfmigrations.json. Defaults to auto-detection based on your server/box.json. */ function run( boolean open = false, boolean boxlang ) { var directory = getCWD(); diff --git a/commands/migrate/install.cfc b/commands/migrate/install.cfc index 957073c..43d7f7b 100644 --- a/commands/migrate/install.cfc +++ b/commands/migrate/install.cfc @@ -1,4 +1,7 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Installs the cfmigrations table in to your database. * * The cfmigrations table keeps track of the migrations ran against your database. @@ -7,9 +10,9 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @manager.hint The Migration Manager to use. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @verbose.hint If true, errors will output a full stack trace. + * @verbose If true, errors will output a full stack trace. */ function run( string manager = "default", boolean verbose = false ) { setup( arguments.manager ); diff --git a/commands/migrate/refresh.cfc b/commands/migrate/refresh.cfc index 963d2df..118a03d 100644 --- a/commands/migrate/refresh.cfc +++ b/commands/migrate/refresh.cfc @@ -1,13 +1,16 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Rollback all committed migrations and then apply all migrations in order. */ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @manager.hint The Migration Manager to use. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @seed.hint If true, runs all seeders for the manager after creating a fresh database. - * @verbose.hint If true, errors output a full stack trace + * @seed If true, runs all seeders for the manager after creating a fresh database. + * @verbose If true, errors output a full stack trace */ function run( string manager = "default", boolean seed = false, boolean verbose = false ) { setup( arguments.manager ); diff --git a/commands/migrate/reset.cfc b/commands/migrate/reset.cfc index 8ae30cb..5392b81 100644 --- a/commands/migrate/reset.cfc +++ b/commands/migrate/reset.cfc @@ -4,9 +4,9 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @manager.hint The Migration Manager to use. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @verbose.hint If true, errors output a full stack trace. + * @verbose If true, errors output a full stack trace. */ function run( string manager = "default", boolean verbose = false ) { setup( arguments.manager ); diff --git a/commands/migrate/seed/create.cfc b/commands/migrate/seed/create.cfc index d0d4282..cbec19c 100644 --- a/commands/migrate/seed/create.cfc +++ b/commands/migrate/seed/create.cfc @@ -1,15 +1,18 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Create a new seeder CFC in an existing application. * Make sure you are running this command in the root of your app. */ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @name.hint Name of the seeder to create without the extension. - * @manager.hint The Migration Manager to use. + * @name Name of the seeder to create without the extension. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @open.hint Open the file once generated. - * @boxlang.hint Create a .bx file instead of a .cfc. Defaults to auto-detection based on your server/box.json. + * @open Open the file once generated. + * @boxlang Create a .bx file instead of a .cfc. Defaults to auto-detection based on your server/box.json. */ function run( required string name, string manager = "default", boolean open = false, boolean boxlang ) { setup( manager = arguments.manager, setupDatasource = false ); diff --git a/commands/migrate/seed/run.cfc b/commands/migrate/seed/run.cfc index 9c971c3..38fce67 100644 --- a/commands/migrate/seed/run.cfc +++ b/commands/migrate/seed/run.cfc @@ -1,4 +1,7 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Runs one or all seeders for an application against your database. * Seeders have no concept of being ran. * Running a seeder multiple times will insert data multiple times. @@ -6,11 +9,11 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @name.hint The name of a seed to run. Runs all seeds if left blank. + * @name The name of a seed to run. Runs all seeds if left blank. * @name.optionsUDF completeSeedNames - * @manager.hint The Migration Manager to use. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @verbose.hint If true, errors output a full stack trace. + * @verbose If true, errors output a full stack trace. */ function run( string name = "", string manager = "default", boolean verbose = false ) { setup( arguments.manager ); diff --git a/commands/migrate/uninstall.cfc b/commands/migrate/uninstall.cfc index 407b989..7931c67 100644 --- a/commands/migrate/uninstall.cfc +++ b/commands/migrate/uninstall.cfc @@ -1,4 +1,7 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Uninstalls the cfmigrations table from your database. * * The cfmigrations table keeps track of the migrations ran against your database. @@ -7,10 +10,10 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @manager.hint The Migration Manager to use. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @verbose.hint If true, errors output a full stack trace. - * @force.hint If true, will not wait for confirmation to uninstall cfmigrations. + * @verbose If true, errors output a full stack trace. + * @force If true, will not wait for confirmation to uninstall cfmigrations. */ function run( string manager = "default", boolean verbose = false, boolean force = false ) { setup( arguments.manager ); diff --git a/commands/migrate/up.cfc b/commands/migrate/up.cfc index a8fb68c..b1a5366 100644 --- a/commands/migrate/up.cfc +++ b/commands/migrate/up.cfc @@ -1,16 +1,19 @@ /** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- * Apply one or all pending migrations against your database. */ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * @manager.hint The Migration Manager to use. + * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers - * @seed.hint If true, runs all seeders for the manager after creating a fresh database. - * @once.hint Only apply a single migration. - * @verbose.hint If true, errors output a full stack trace. - * @pretend.hint If true, only pretends to run the query. The SQL that would have been run is printed to the console. - * @file.hint If provided, outputs the SQL that would have been run to the file. Only applies when running `pretend`. + * @seed If true, runs all seeders for the manager after creating a fresh database. + * @once Only apply a single migration. + * @verbose If true, errors output a full stack trace. + * @pretend If true, only pretends to run the query. The SQL that would have been run is printed to the console. + * @file If provided, outputs the SQL that would have been run to the file. Only applies when running `pretend`. */ function run( string manager = "default", diff --git a/models/BaseMigrationCommand.cfc b/models/BaseMigrationCommand.cfc index 5f6adbd..0e0293e 100644 --- a/models/BaseMigrationCommand.cfc +++ b/models/BaseMigrationCommand.cfc @@ -1,3 +1,8 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + */ component { property name="fileSystemUtil" inject="FileSystem"; From cba7bb12de6bc03b7fb4e7106bd5531592fc42c1 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 16 Jun 2026 22:13:31 +0200 Subject: [PATCH 06/14] more boxlang specific options --- .bxlint.json | 68 ++++++++++++++++++++++++++++++++ commands/migrate/create.cfc | 26 ++++++------ commands/migrate/init.cfc | 27 +++++++------ commands/migrate/seed/create.cfc | 26 ++++++------ models/BaseMigrationCommand.cfc | 10 +++-- templates/MigrationBX.txt | 11 ++++++ templates/config.txt | 2 +- templates/seedBX.txt | 7 ++++ 8 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 .bxlint.json create mode 100644 templates/MigrationBX.txt create mode 100644 templates/seedBX.txt diff --git a/.bxlint.json b/.bxlint.json new file mode 100644 index 0000000..e5d1d43 --- /dev/null +++ b/.bxlint.json @@ -0,0 +1,68 @@ +{ + "diagnostics": { + "duplicateMethod": { + "enabled": true, + "severity": "error" + }, + "duplicateProperty": { + "enabled": true, + "severity": "error" + }, + "emptyCatchBlock": { + "enabled": true, + "severity": "warning" + }, + "invalidExtends": { + "enabled": false, + "severity": "error" + }, + "invalidImplements": { + "enabled": false, + "severity": "error" + }, + "missingQueryParamCfsqltype": { + "enabled": true, + "severity": "warning" + }, + "missingReturnStatement": { + "enabled": true, + "severity": "warning" + }, + "shadowedVariable": { + "enabled": true, + "severity": "warning" + }, + "unescapedQueryParam": { + "enabled": true, + "severity": "warning" + }, + "unreachableCode": { + "enabled": true, + "severity": "warning" + }, + "unscopedVariable": { + "enabled": true, + "severity": "warning" + }, + "unusedImport": { + "enabled": true, + "severity": "warning" + }, + "unusedPrivateMethod": { + "enabled": true, + "severity": "info" + }, + "unusedVariable": { + "enabled": true, + "severity": "hint" + } + }, + "include": [], + "exclude": [], + "mappings": {}, + "formatting": { + "experimental": { + "enabled": false + } + } +} \ No newline at end of file diff --git a/commands/migrate/create.cfc b/commands/migrate/create.cfc index c4875e7..5e66a8d 100644 --- a/commands/migrate/create.cfc +++ b/commands/migrate/create.cfc @@ -21,35 +21,31 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { required string name, string manager = "default", boolean open = false, - boolean boxlang + boolean boxlang = isBoxLangProject( getCWD() ) ) { - setup( manager = arguments.manager, setupDatasource = false ); - - var migrationsDirectory = expandPath( variables.migrationService.getMigrationsDirectory() ); + setup( manager = arguments.manager, setupDatasource = false ) + var migrationsDirectory = expandPath( variables.migrationService.getMigrationsDirectory() ) // Validate migrationsDirectory if ( !directoryExists( migrationsDirectory ) ) { - directoryCreate( migrationsDirectory ); + directoryCreate( migrationsDirectory ) } - var useBoxLang = arguments.keyExists( "boxlang" ) ? arguments.boxlang : isBoxLangProject( getCWD() ); - var extension = useBoxLang ? "bx" : "cfc"; - - var timestamp = dateTimeFormat( now(), "yyyy_mm_dd_HHnnss" ); - var migrationPath = "#migrationsDirectory##timestamp#_#arguments.name#.#extension#"; - - var migrationContent = fileRead( "/commandbox-migrations/templates/Migration.txt" ); + var extension = arguments.boxlang ? "bx" : "cfc" + var timestamp = dateTimeFormat( now(), "yyyy_mm_dd_HHnnss" ) + var migrationPath = "#migrationsDirectory##timestamp#_#arguments.name#.#extension#" + var migrationContent = fileRead( "/commandbox-migrations/templates/Migration#arguments.boxlang ? "BX" : ""#.txt" ) file action="write" file="#migrationPath#" mode="777" output="#trim( migrationContent )#"; - print.greenLine( "Created #migrationPath#" ); + print.greenLine( "Created #migrationPath#" ) // Open file? if ( arguments.open ) { - openPath( migrationPath ); + openPath( migrationPath ) } - return; + return } } diff --git a/commands/migrate/init.cfc b/commands/migrate/init.cfc index 45554ed..0c7bae0 100644 --- a/commands/migrate/init.cfc +++ b/commands/migrate/init.cfc @@ -10,30 +10,35 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** + * Initialize your project to use commandbox-migrations + * Make sure you are running this command in the root of your app. + * + * @open Open the config file after it is created. * @boxlang Create a .bxmigrations.json file instead of .cfmigrations.json. Defaults to auto-detection based on your server/box.json. */ - function run( boolean open = false, boolean boxlang ) { - var directory = getCWD(); - - var useBoxLang = arguments.keyExists( "boxlang" ) ? arguments.boxlang : isBoxLangProject( directory ); - var configFileName = useBoxLang ? ".bxmigrations.json" : ".cfmigrations.json"; - var configPath = "#directory#/#configFileName#"; + function run( + boolean open = false, + boolean boxlang = isBoxLangProject( getCWD() ) + ) { + var directory = getCWD() + var configFileName = arguments.boxlang ? ".bxmigrations.json" : ".cfmigrations.json" + var configPath = "#directory#/#configFileName#" // Check and see if the config file already exists if ( fileExists( configPath ) ) { - print.yellowLine( "#configFileName# already exists." ); - return; + print.yellowLine( "#configFileName# already exists." ) + return } - var configStub = fileRead( "/commandbox-migrations/templates/config.txt" ); + var configStub = fileRead( "/commandbox-migrations/templates/config.txt" ) file action="write" file="#configPath#" mode="777" output="#trim( configStub )#"; - print.greenLine( "Created #configFileName# config file." ); + print.greenLine( "Created #configFileName# config file." ) // Open file? if ( arguments.open ) { - openPath( configPath ); + openPath( configPath ) } } diff --git a/commands/migrate/seed/create.cfc b/commands/migrate/seed/create.cfc index cbec19c..6cd2e0a 100644 --- a/commands/migrate/seed/create.cfc +++ b/commands/migrate/seed/create.cfc @@ -14,30 +14,32 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { * @open Open the file once generated. * @boxlang Create a .bx file instead of a .cfc. Defaults to auto-detection based on your server/box.json. */ - function run( required string name, string manager = "default", boolean open = false, boolean boxlang ) { - setup( manager = arguments.manager, setupDatasource = false ); + function run( + required string name, + string manager = "default", + boolean open = false, + boolean boxlang = isBoxLangProject( getCWD() ) + ) { + setup( manager = arguments.manager, setupDatasource = false ) - var seedsDirectory = expandPath( variables.migrationService.getSeedsDirectory() ); + var seedsDirectory = expandPath( variables.migrationService.getSeedsDirectory() ) // Validate seedsDirectory if ( !directoryExists( seedsDirectory ) ) { - directoryCreate( seedsDirectory ); + directoryCreate( seedsDirectory ) } - var useBoxLang = arguments.keyExists( "boxlang" ) ? arguments.boxlang : isBoxLangProject( getCWD() ); - var extension = useBoxLang ? "bx" : "cfc"; - - var seedPath = "#seedsDirectory##arguments.name#.#extension#"; - - var seedContent = fileRead( "/commandbox-migrations/templates/seed.txt" ); + var extension = arguments.boxlang ? "bx" : "cfc" + var seedPath = "#seedsDirectory##arguments.name#.#extension#" + var seedContent = fileRead( "/commandbox-migrations/templates/seed#arguments.boxlang ? "BX" : ""#.txt" ) file action="write" file="#seedPath#" mode="777" output="#trim( seedContent )#"; - print.greenLine( "Created #seedPath#" ); + print.greenLine( "Created #seedPath#" ) // Open file? if ( arguments.open ) { - openPath( seedPath ); + openPath( seedPath ) } } diff --git a/models/BaseMigrationCommand.cfc b/models/BaseMigrationCommand.cfc index 0e0293e..25b5b12 100644 --- a/models/BaseMigrationCommand.cfc +++ b/models/BaseMigrationCommand.cfc @@ -114,23 +114,27 @@ component { /** * Detects whether the given directory should be treated as a BoxLang project, * based on the running CommandBox server's engine or box.json's `language` key. + * + * @directory The directory to check for box.json (usually the current working directory). + * + * @return True if this is a BoxLang project, false otherwise. */ private boolean function isBoxLangProject( required string directory ) { // Detect if the running CommandBox server is BoxLang. var serverInfo = variables.serverService.resolveServerDetails( {} ).serverInfo; if ( serverInfo.keyExists( "cfengine" ) && serverInfo.cfengine contains "boxlang" ) { - return true; + return true } // Detect via box.json's language key. if ( packageService.isPackage( arguments.directory ) ) { var boxJSON = packageService.readPackageDescriptor( arguments.directory ); if ( boxJSON.keyExists( "language" ) && boxJSON.language == "boxlang" ) { - return true; + return true } } - return false; + return false } /** diff --git a/templates/MigrationBX.txt b/templates/MigrationBX.txt new file mode 100644 index 0000000..a18b475 --- /dev/null +++ b/templates/MigrationBX.txt @@ -0,0 +1,11 @@ +class { + + function up( schema, qb ) { + + } + + function down( schema, qb ) { + + } + +} diff --git a/templates/config.txt b/templates/config.txt index 115a016..76dea33 100644 --- a/templates/config.txt +++ b/templates/config.txt @@ -6,7 +6,7 @@ "properties": { "defaultGrammar": "AutoDiscover@qb", "schema": "${DB_SCHEMA}", - "migrationsTable": "cfmigrations", + "migrationsTable": "cbmigrations", "connectionInfo": { "type": "${DB_DRIVER}", "database": "${DB_DATABASE}", diff --git a/templates/seedBX.txt b/templates/seedBX.txt new file mode 100644 index 0000000..590fdff --- /dev/null +++ b/templates/seedBX.txt @@ -0,0 +1,7 @@ +class { + + function run( qb, mockdata ) { + + } + +} From 0982afae98c1c818cb126ca0b303a80efbecb14c Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 16 Jun 2026 22:45:24 +0200 Subject: [PATCH 07/14] feat: Addd AI integrations, skills and instructions --- .gitignore | 6 +- AGENTS.md | 98 ++++++++++++++++ box.json | 6 +- skills-lock.json | 299 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 AGENTS.md create mode 100644 skills-lock.json diff --git a/.gitignore b/.gitignore index ea99fc0..b2b8fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ TASKS.md jmimemagic.log -.vscode \ No newline at end of file +.vscode + +## AI +.opencode/** +.agents/skills/** \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..38c4ed3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,98 @@ +# Project Guidelines + +## Code Style + +This is a **CommandBox module** written in CFML (`.cfc`) with BoxLang (`.bx`) templates for scaffolding. Follow the **Ortus Coding Standards** skill (`ortus-coding-standards`) for all formatting — the key rules are: + +- **Tabs** for indentation, never spaces. +- **K&R brace style** — opening brace on the same line as the statement. +- **Always use braces** — even for single-statement `if`/`for`/`while`. +- **Spaces inside parentheses** — `function process( name, count )` not `function process(name, count)`. +- **No space between function name and `(`** — `doThing( name )` not `doThing ( name )`. +- **One space around operators** — `var total = price * quantity`. +- **Align related assignments** in column groups. +- **Always use braces** — even for single-statement bodies, never inline bodies on the same line as the condition. + +Semicolons are **optional** in CFML/BoxLang — match the surrounding file's convention (most files omit them). + +Arrow functions use the fat-arrow syntax: `( migration ) => { ... }` + +CFML code is formatted via **CFFormat** (`.cfformat.json`). After editing `.cfc` files, run: +``` +box run-script format +``` + +BoxLang templates (`.bx`) are linted via `.bxlint.json`. + +## Architecture + +This is a **CommandBox CLI module** that wraps [cfmigrations](https://github.com/coldbox-modules/cfmigrations) to provide `migrate` commands. Key structure: + +- `commands/migrate/` — CommandBox commands (each `.cfc` has a `run()` method). Nested folders are sub-commands (e.g., `seed/run.cfc` → `migrate seed run`). +- `models/BaseMigrationCommand.cfc` — Shared base class all commands extend. Contains `setup()`, `getMigrationsInfo()`, `isBoxLangProject()`, config resolution, and datasource registration. +- `ModuleConfig.cfc` — WireBox module lifecycle; registers `SqlHighlighter` singleton. +- `templates/` — Scaffolding templates for new migrations/seeds. `.txt` = CFML templates, `BX.txt` suffix = BoxLang templates. +- `lib/sql.nanorc` — jLine syntax highlighting rules for SQL output in CLI. + +**Dependency Injection**: WireBox via `property name="x" inject="Y"` annotations. Key services: `FileSystem`, `PackageService`, `JSONService`, `SystemSettings`, `ServerService`, `Formatter@sqlFormatter`, `SqlHighlighter`. + +**Config resolution order**: `.cbmigrations.json` → legacy `.cfmigrations.json` → legacy `box.json` `cfmigrations` key (deprecated, auto-converted). Environment variables are expanded via `systemSettings.expandDeepSystemSettings()`. + +**Dual-language support**: Auto-detects BoxLang via running server engine or `box.json` `language` key. When generating scaffolding, select the correct template pair (`Migration.txt` vs `MigrationBX.txt`). + +## Build and Test + +```bash +# Format CFML files +box run-script format + +# No test suite exists — this project relies on manual/CLI testing. +``` + +The only automated script is `format` (CFFormat). Release flow: format → publish to ForgeBox → git push with tags. + +## Conventions + +### Copyright Header +Every `.cfc` and `.bx` file starts with: +``` +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + */ +``` + +### Command Structure +All commands follow this pattern: +1. `extends="commandbox-migrations.models.BaseMigrationCommand"` +2. JavaDoc-style `/** */` parameter annotations with `@paramName.optionsUDF completeXxx` for tab-completion wiring +3. `run()` method calls `setup( arguments.manager )` first +4. Optional verbose diagnostics block +5. `pagePoolClear()` before migration execution +6. `try`/`catch` with SQL formatting and highlighting in error output +7. Pre/post process hooks via lambdas for migration logging and SQL capture +8. `pretend` mode captures SQL without executing + +### Error Handling Pattern +```js +catch ( any e ) { + if ( arguments.verbose ) { + if ( structKeyExists( e, "Sql" ) ) { + print.whiteOnRedLine( "Error when trying to ..." ); + print.line( variables.sqlHighlighter.highlight( variables.sqlFormatter.format( e.Sql ) ).toAnsi() ); + } + rethrow; + } + return error( e.message, e.detail ); +} +``` + +### Naming + +| Item | Convention | Example | +|------|-----------|---------| +| Command CFCs | Lowercase verb | `up.cfc`, `down.cfc`, `fresh.cfc` | +| Models | PascalCase | `BaseMigrationCommand.cfc` | +| Templates | PascalCase, `BX` suffix | `Migration.txt` / `MigrationBX.txt` | +| Config files | Dot-prefixed JSON | `.cbmigrations.json`, legacy `.cfmigrations.json` | \ No newline at end of file diff --git a/box.json b/box.json index f60e33a..c1278fa 100644 --- a/box.json +++ b/box.json @@ -1,5 +1,5 @@ { - "name":"CFMigrations Commands", + "name":"ColdBox Migrations Commands", "version":"5.3.0", "location":"forgeboxStorage", "author":"Eric Peterson", @@ -11,14 +11,14 @@ }, "bugs":"https://github.com/commandbox-modules/commandbox-migrations/issues", "slug":"commandbox-migrations", - "shortDescription":"Run your cfmigrations from CommandBox", + "shortDescription":"Run your ColdBox migrations from CommandBox", "instructions":"https://github.com/commandbox-modules/commandbox-migrations", "type":"commandbox-modules", "keywords":[ "server", "database", "migrations", - "cfmigrations" + "cbmigrations" ], "scripts":{ "onRelease":"publish", diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..e8b709d --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,299 @@ +{ + "version": 1, + "skills": { + "boxlang-application-descriptor": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/application-descriptor/SKILL.md", + "computedHash": "f060db9e4cfd932f4977f70c079555362aadfcbdd2fca250a04d7f8fb471207a" + }, + "boxlang-async-programming": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/async-programming/SKILL.md", + "computedHash": "51b310a5f862faf03ff930b8854566ce697ef5706014ea0b8d41fd678f9aa6d5" + }, + "boxlang-best-practices": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/best-practices/SKILL.md", + "computedHash": "be3060bb9d86e46c36589d70b83a178737bba80e69348de8846301f6160290b5" + }, + "boxlang-cfml-migration": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/cfml-migration/SKILL.md", + "computedHash": "a284466dc5cc392747b8e46a206ee162acc14bfe3e3c3b25200cecc07d1129ea" + }, + "boxlang-classes-and-oop": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/classes-and-oop/SKILL.md", + "computedHash": "17477293bf20faebb024944240c91a838c169d1a705ab0f4ccfa9c04fd9897c0" + }, + "boxlang-code-documenter": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/code-documenter/SKILL.md", + "computedHash": "ac06957c446282e603c5ba566c893618983a3dfea7f49df6986d5048824aef17" + }, + "boxlang-code-reviewer": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/code-reviewer/SKILL.md", + "computedHash": "75b1a6891e8e4c5f73f5251956f6ef7bfaf497289cf13cdbad277e7a5bf8d55d" + }, + "boxlang-configuration": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/configuration/SKILL.md", + "computedHash": "62ea714d8d806324b56ed0d656e9f21663e756ede9e52e53c120962f924e4eb5" + }, + "boxlang-database-access": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/database-access/SKILL.md", + "computedHash": "44e26ffcf4dcd633eedd4d08aaa1dc0d1ad5aac700d6798b324e7b3e1c8688f0" + }, + "boxlang-docbox": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/docbox/SKILL.md", + "computedHash": "f68d2cdd4e2ed9cf23005c466bb50b119794f449c61eeb7bb32469c03a3e7fe3" + }, + "boxlang-file-handling": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/file-handling/SKILL.md", + "computedHash": "134f2e5c3a1cd7fb14f971cc2580f7c0fa1046e85f986232314adefd652f4f7c" + }, + "boxlang-file-watchers": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/file-watchers/SKILL.md", + "computedHash": "db6db2a78806f93e6a58d24895ea9373a0dbbe29809479dd9dc50209cf9f9819" + }, + "boxlang-functional-programming": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/functional-programming/SKILL.md", + "computedHash": "d8e4d8b2f4f3f6bd1cb43cd557945c0e7a6d68340b962f2bcc4d2a5c2012a546" + }, + "boxlang-interceptors": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/interceptors/SKILL.md", + "computedHash": "18e163de09ae7cfb0ab0f2b4d74d97c58f5ace239630d57b8505909373f5bc84" + }, + "boxlang-java-integration": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/java-integration/SKILL.md", + "computedHash": "f0e6a9dd347c4cbca99aae570fd2ddaa46751d0175767f3cc08d57b66a8ba10b" + }, + "boxlang-language-fundamentals": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/language-fundamentals/SKILL.md", + "computedHash": "1791856caa11bad9436ea0229eaa45f1505a6d26f030f5c244dfbcd2dd7fe4da" + }, + "boxlang-modules-and-packages": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/modules-and-packages/SKILL.md", + "computedHash": "d2291df901e39f82c9bc1225ceb04a0c2046b5ec06a596e4a136942ec85b2910" + }, + "boxlang-runtime-cli-scripting": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/runtime-cli-scripting/SKILL.md", + "computedHash": "e8a3704ba54ece31d36ceb52744a79618ba8be436018ef30aaf673291e20de96" + }, + "boxlang-runtime-commandbox": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/runtime-commandbox/SKILL.md", + "computedHash": "8744cc111e9146cf929e0d2f5c4344f871a06ebfe69e8b7d5cb532b9c17274e2" + }, + "boxlang-scheduled-tasks": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/scheduled-tasks/SKILL.md", + "computedHash": "534448e61aacce705de4065549cfccd9cf8bb1c552330fddd3b40b200960ab58" + }, + "boxlang-security": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/security/SKILL.md", + "computedHash": "bcf72fd5f5d27c40efef3e6f4ee33017497f0b6bc54e35ae59b3a485b824af0f" + }, + "boxlang-templating": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/templating/SKILL.md", + "computedHash": "92712aa9582312ebd5ed430d8966aa45a47996cb3ec9679cfb401a88085eae63" + }, + "boxlang-testing": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/testing/SKILL.md", + "computedHash": "5062447bfb00f97d62536e8f83e78f5e0e05ea9b01f1ab542b9d29c6a4a7224c" + }, + "boxlang-web-development": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/web-development/SKILL.md", + "computedHash": "365a7dea08d0aa06af2dec134ef5a6c67fe4cc622e9856e2a10426a65ec90de5" + }, + "boxlang-zip": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/zip/SKILL.md", + "computedHash": "06f346e32aacdccfd501222341cca88b3226e3703123e6e8053495e058bc9f0a" + }, + "commandbox-config-settings": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-config-settings/SKILL.md", + "computedHash": "19dfd2dbd842e0c4fa428e5c7921fd36c996b56d94345daa308be32293c12ab2" + }, + "commandbox-deploying": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-deploying/SKILL.md", + "computedHash": "81ed1fdd292fb9ad29ed7daa1dba9f7daa722bd684d9b2cf7fc96faa7387cefd" + }, + "commandbox-developing": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-developing/SKILL.md", + "computedHash": "c18b0d4ceaf9c03edb5ed9a61b59d1a696014744126929017fc2856c90b7c709" + }, + "commandbox-embedded-server": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-embedded-server/SKILL.md", + "computedHash": "ffb7bc1079ab5fc3361bf2a64427030ecef5ae4c74ef0e9bfecb6c210b14200b" + }, + "commandbox-package-management": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-package-management/SKILL.md", + "computedHash": "63b73016c804250f9e05104d4c87015071b0bbbe84af268369d1d3243f9c4548" + }, + "commandbox-setup": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-setup/SKILL.md", + "computedHash": "8ae2ee6f93d668e4a11d9bd57d72d3b9f68f1cb3f9e2d780b6f2675463371bf3" + }, + "commandbox-task-runners": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-task-runners/SKILL.md", + "computedHash": "a32c3fe1969f261e789e732ca68e4c5e2e9383b35d0be20ed1fba473ea95feef" + }, + "commandbox-testing": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-testing/SKILL.md", + "computedHash": "15dfb0ca0d64edfd0ec0344df2f9b3494a8a15a666a464927148f43deaa5b585" + }, + "commandbox-usage": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "commandbox/commandbox-usage/SKILL.md", + "computedHash": "db689cb9f7f895a202ecae3986f24557ff0e57d4c614da6919678204e995dd87" + }, + "github-action-authoring": { + "source": "ortus-solutions/skills", + "sourceType": "github", + "skillPath": "github-action-authoring/SKILL.md", + "computedHash": "2a68bac1399c4a54c88fd7d2596b1f3e0cd68461b2dca33335c2210504c9dd8b" + }, + "java-expert": { + "source": "ortus-solutions/skills", + "sourceType": "github", + "skillPath": "java-expert/SKILL.md", + "computedHash": "f622426ac4233ec79888d7c2b87c966efa41fed4dd06fbe24f77b86c3da79079" + }, + "junit-expert": { + "source": "ortus-solutions/skills", + "sourceType": "github", + "skillPath": "junit-expert/SKILL.md", + "computedHash": "5ec181a33d4888873dbbc3fc56308fc4ca76dd630e21022284a13f07f092f6c9" + }, + "ortus-coding-standards": { + "source": "ortus-boxlang/skills", + "sourceType": "github", + "skillPath": "boxlang-developer/ortus-coding-standards/SKILL.md", + "computedHash": "01480f77e70756f2029bf159a79acf3ab079f0159ea7c4eaff499a28fcacf6b5" + }, + "testbox-assertions": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/assertions/SKILL.md", + "computedHash": "694ca3ce806c087739e3bff4aaa14056be7a5401a8744b43f97ed9e9f89d8450" + }, + "testbox-bdd": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/bdd/SKILL.md", + "computedHash": "b74bf8329dfa96959df21b0737e78499269fa45256978d047643e2a258de403d" + }, + "testbox-cbmockdata": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/cbmockdata/SKILL.md", + "computedHash": "899b465c01b9257720f6d04834ffc567a8ad4381ece2f807ec431ae887d0a132" + }, + "testbox-expectations": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/expectations/SKILL.md", + "computedHash": "b45d34d7a74fbed11455f2e1906b5d1477a33f313900cc8066d241ad9853b5f8" + }, + "testbox-listeners": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/listeners/SKILL.md", + "computedHash": "ca3d8a2120e5362592827cf22e5712c8f64a79d3da9392a829ffea6f78e9a221" + }, + "testbox-mockbox": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/mockbox/SKILL.md", + "computedHash": "2aeb189901f43e79249efccd49eb0771c42438f4248de3d631f4b3da7b445a50" + }, + "testbox-reporters": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/reporters/SKILL.md", + "computedHash": "89da95096b7a7c4f2f2da8a18a2d701056df0321bac5962f11fa48167541ec1b" + }, + "testbox-runners": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/runners/SKILL.md", + "computedHash": "048a646907313fae5b94ddc6ea3d88019c0873570b7c1c89efa885ec68ddf305" + }, + "testbox-unit-xunit": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/unit/SKILL.md", + "computedHash": "a8b371d1d7dab879ac85120bdf83e2972a5a6b1d99efb4bd39d92fa025fdcd13" + }, + "testing-coverage": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/testing-coverage/SKILL.md", + "computedHash": "912f7ee7a7f2e820a646cb0913257d0b086c1f49ed3033e9007136920c7f5b9a" + }, + "testing-fixtures": { + "source": "coldbox/skills", + "sourceType": "github", + "skillPath": "testbox/testing-fixtures/SKILL.md", + "computedHash": "4393c328b30d2201a5419f8ea4aee8186e451cb90ea196d8bf77ffd47355e2f2" + } + } +} From 165dffa51a9294ac956c77ae25ddd867827de444 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 16 Jun 2026 22:53:13 +0200 Subject: [PATCH 08/14] feat!: Updated to use the new file format name of cbmigrations instead of cfmigrations to provide abstractions. --- .markdownlint.json | 15 +++++ README.md | 102 +++++++++++++++++++++----------- commands/migrate/down.cfc | 2 +- commands/migrate/fresh.cfc | 2 +- commands/migrate/init.cfc | 6 +- commands/migrate/install.cfc | 7 +-- commands/migrate/refresh.cfc | 2 +- commands/migrate/reset.cfc | 2 +- commands/migrate/seed/run.cfc | 4 +- commands/migrate/uninstall.cfc | 12 ++-- commands/migrate/up.cfc | 2 +- models/BaseMigrationCommand.cfc | 45 +++++++++----- 12 files changed, 128 insertions(+), 73 deletions(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..9f231b5 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,15 @@ +{ + "line-length": false, + "single-h1": false, + "no-hard-tabs" : false, + "fenced-code-language" : false, + "no-bare-urls" : false, + "first-line-h1": false, + "no-multiple-blanks": { + "maximum": 2 + }, + "no-duplicate-header" : false, + "no-duplicate-heading" : false, + "no-inline-html" : false, + "no-emphasis-as-heading": false +} diff --git a/README.md b/README.md index 42f1e2e..81257db 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,58 @@ -# `commandbox-migrations` +# CommandBox Migrations -## Run your [`cfmigrations`](https://github.com/elpete/cfmigrations) from CommandBox +CommandBox Migrations is a [CommandBox](https://www.ortussolutions.com/products/commandbox) module that lets you run database migrations directly from the CLI. It wraps the [ColdBox Migrations](https://github.com/coldbox-modules/cfmigrations) library, giving you a powerful, convention-driven way to evolve your database schema — without needing a running web server. +## Features + +- **CLI-driven** — Run migrations from anywhere using `migrate up`, `migrate down`, `migrate fresh`, and more. +- **Multi-manager support** — Manage multiple named migration configurations (e.g., `default`, `alternate`) from a single project. +- **Seeding** — Create and run database seeds with `migrate seed create` and `migrate seed run`. +- **BoxLang support** — Full support for BoxLang projects with automatic detection and `.cbmigrations.json` config. +- **Environment-aware** — Leverage `${ENV_VAR}` placeholders in your config for database credentials and settings. +- **Init scaffolding** — Get up and running fast with `migrate init` to generate your config and first migration. + +## Upgrading to v6.0.0? + +v6 makes `.cbmigrations.json` the **universal standard** config file for **all** projects — BoxLang and CFML alike. The legacy `.cfmigrations.json` is fully deprecated. + +### What changed + +- **Config file:** `.cbmigrations.json` is now the one and only config file name. If `migrate init` finds only `.cfmigrations.json`, it will prompt you to rename it automatically. If both files exist, `.cbmigrations.json` takes priority. +- **Migration table:** The default migration table name is now `cbmigrations` (was `cfmigrations` in v4/v5). New projects created with `migrate init` will use `cbmigrations` by default. +- **BoxLang support:** All the BoxLang support introduced in v5 is now fully mature. The `--boxlang` / `--no-boxlang` flags work identically, but `.cbmigrations.json` is used for both BoxLang and CFML projects alike. + +### How to upgrade + +1. Rename your `.cfmigrations.json` to `.cbmigrations.json` (or let `migrate init` do it for you). +2. If you are starting fresh, your migration table will default to `cbmigrations`. If you have an existing `cfmigrations` table, keep the `"migrationsTable": "cfmigrations"` setting in your config. + +## Upgrading to v5.0.0? + +v5 introduced first-class [BoxLang](https://boxlang.io) support alongside the CFML experience. + +### What changed + +- **BoxLang detection:** The module now auto-detects BoxLang projects based on the running server engine or the `"language": "boxlang"` key in `box.json`. +- **Dual config support:** `.cbmigrations.json` was introduced as the BoxLang config file. If both `.cbmigrations.json` and `.cfmigrations.json` exist, the `.cbmigrations.json` file is read first. +- **Scaffolding:** `migrate create` and `migrate seed create` generate `.bx` files for BoxLang projects (auto-detected, or overridden with `--boxlang` / `--no-boxlang`). +- **`migrate init --boxlang`:** The init command gained `--boxlang` / `--no-boxlang` flags to override auto-detection. + +### How to upgrade + +Upgrading from v4 is straightforward — no breaking config changes. If you're a BoxLang user, your project will be auto-detected and you'll get `.bx` scaffolding automatically. If you're a CFML user, nothing changes. ## Upgrading to v4.0.0? +> ⚠️ **Legacy:** v4 introduced the `.cfmigrations.json` config file. As of v6, the standard config file is `.cbmigrations.json` for all projects. See [Upgrading to v6.0.0?](#upgrading-to-v600) for details. + v4 brings a new configuration structure and file. This pairs with new features in CFMigrations to allow for multiple named migration managers and new seeding capabilities. Migrations will still run in v4 using the old configuration structure and location, but it is highly recommended you upgrade. You can create the new `.cfmigrations.json` config file by running `migrate init`. -> If your project is a [BoxLang](https://boxlang.io) project, the config file will be named -> `.bxmigrations.json` instead. `.bxmigrations.json` is always checked first if both files exist. -> BoxLang detection is automatic (based on a running BoxLang server or a `"language": "boxlang"` -> entry in your `box.json`), or you can force it with `migrate init --boxlang` / `--no-boxlang`. - -The new config file format mirrors `CFMigrations`: +> In v5, `.cbmigrations.json` was introduced for BoxLang projects alongside `.cfmigrations.json`. +> As of v6, `.cbmigrations.json` is the universal standard for all projects. ```json { @@ -27,7 +63,7 @@ The new config file format mirrors `CFMigrations`: "properties": { "defaultGrammar": "MySQLGrammar@qb", "schema": "${DB_SCHEMA}", - "migrationsTable": "cfmigrations", + "migrationsTable": "cbmigrations", "connectionInfo": { "host": "${DB_HOST}", "username": "${DB_USER}", @@ -51,7 +87,7 @@ More managers can be added as new top-level keys: "properties": { "defaultGrammar": "MySQLGrammar@qb", "schema": "${DB_SCHEMA}", - "migrationsTable": "cfmigrations", + "migrationsTable": "cbmigrations", "connectionInfo": { "host": "${DB_HOST}", "username": "${DB_USER}", @@ -68,7 +104,7 @@ More managers can be added as new top-level keys: "properties": { "defaultGrammar": "MySQLGrammar@qb", "schema": "${DB_SCHEMA}", - "migrationsTable": "cfmigrations2", + "migrationsTable": "cbmigrations_alternate", "connectionInfo": { "host": "${DB_HOST}", "username": "${DB_USER}", @@ -89,11 +125,7 @@ Make sure to append `@qb` to the end of any qb-supplied grammars, like `AutoDisc ## Setup -You need to create a `.cfmigrations.json` config file in your application root folder. You can do this easily by running `migrate init`: - -> If your project is a [BoxLang](https://boxlang.io) project, `migrate init` will create -> `.bxmigrations.json` instead, and it will be checked first if both files happen to exist. -> See [BoxLang Support](#boxlang-support) below for details on detection and overrides. +You need to create a `.cbmigrations.json` config file in your application root folder. You can do this easily by running `migrate init`. ```json { @@ -104,7 +136,7 @@ You need to create a `.cfmigrations.json` config file in your application root f "properties": { "defaultGrammar": "MySQLGrammar@qb", "schema": "${DB_SCHEMA}", - "migrationsTable": "cfmigrations", + "migrationsTable": "cbmigrations", "connectionInfo": { "host": "${DB_HOST}", "username": "${DB_USER}", @@ -137,9 +169,9 @@ The `migrationsDirectory` sets the default location for the migration scripts. The `seedsDirectory` sets the default location for the seeder scripts. This setting is optional. > When using MySQL with CommandBox 5 or greater, two additional elements are required in the `connectionInfo` struct: -> `"bundleName":"com.mysql.cj"` and `"bundleVersion":"8.0.15"` +> `"bundleName":"com.mysql.cj"` and `"bundleVersion":"8.0.15"`. These will dissapear in the next iteration as we migrate to BoxLang. -`commandbox-migrations` will create a datasource named `cfmigrations` from the information you specify. +`commandbox-migrations` will create a datasource named `cbmigrations` from the information you specify. You can use this in your queries: ```js @@ -152,11 +184,11 @@ queryExecute( ) ", [], - { datasource = "cfmigrations" } + { datasource = "cbmigrations" } ) ``` -`commandbox-migrations` will also set `cfmigrations` as the default datasource, so the following will work as well: +`commandbox-migrations` will also set `cbmigrations` as the default datasource, so the following will work as well: ```js queryExecute( " @@ -192,10 +224,9 @@ DB_USER=test DB_PASSWORD=pass1234 ``` - I recommend adding this file to your `.gitignore` -``` +```bash .env ``` @@ -211,19 +242,18 @@ DB_PASSWORD= You would update your `.gitignore` file to not ignore the `.env.example` file: -``` +```bash .env !.env.example ``` ## BoxLang Support -`commandbox-migrations` supports [BoxLang](https://boxlang.io) projects: +`commandbox-migrations` fully supports [BoxLang](https://boxlang.io) projects: -- Config file: `migrate init` writes `.bxmigrations.json` instead of `.cfmigrations.json`. - If both files exist, `.bxmigrations.json` is read first. -- Migration/seed files: `migrate create` and `migrate seed create` scaffold `.bx` files - instead of `.cfc` files. +- Scaffolding: `migrate create` and `migrate seed create` generate `.bx` files + instead of `.cfc` files when a BoxLang project is detected (or when `--boxlang` is passed). +- Config: `.cbmigrations.json` is used for all projects (BoxLang and CFML alike) as of v6. Whether a project is treated as BoxLang is auto-detected by checking, in order: @@ -236,14 +266,14 @@ You can skip auto-detection and force the behavior on any of `migrate init`, ## Usage Every command below accepts an optional `manager` argument (defaulting to `default`) -to target a specific named manager from your config file. See -[Upgrading to v4.0.0?](#upgrading-to-v400) above for how to define multiple managers. +to target a specific named manager from your config file. See the config examples +in the upgrade sections above for how to define multiple managers. ### `migrate init [--open] [--boxlang] [--no-boxlang]` -Creates the migration config file, if it doesn't already exist. Creates -`.cfmigrations.json` by default, or `.bxmigrations.json` for BoxLang -projects. Pass `--boxlang`/`--no-boxlang` to override auto-detection. +Creates the migration config file (`.cbmigrations.json`) if it doesn't already exist. +Pass `--boxlang`/`--no-boxlang` to control whether `.bx` or `.cfc` scaffolding is +generated by `migrate create` and `migrate seed create`. Passing `--open` opens the config file once it's created. @@ -310,7 +340,7 @@ given manager (delegates to `migrate down` and `migrate up`, forwarding ### `migrate reset [manager] [--verbose]` -Clears out all objects from the database, including the `cfmigrations` table, +Clears out all objects from the database, including the `cbmigrations` table, for the given manager. Use this when your database is in an inconsistent state in development. @@ -325,7 +355,7 @@ migrated database. ### `migrate uninstall [manager] [--verbose] [--force]` -Removes the `cfmigrations` table for the given manager after running down +Removes the `cbmigrations` table for the given manager after running down any ran migrations. Prompts for confirmation before uninstalling unless `--force` is passed. diff --git a/commands/migrate/down.cfc b/commands/migrate/down.cfc index a26a631..940763b 100644 --- a/commands/migrate/down.cfc +++ b/commands/migrate/down.cfc @@ -24,7 +24,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } diff --git a/commands/migrate/fresh.cfc b/commands/migrate/fresh.cfc index ada2ba7..0096284 100644 --- a/commands/migrate/fresh.cfc +++ b/commands/migrate/fresh.cfc @@ -16,7 +16,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } diff --git a/commands/migrate/init.cfc b/commands/migrate/init.cfc index 0c7bae0..1905df3 100644 --- a/commands/migrate/init.cfc +++ b/commands/migrate/init.cfc @@ -14,14 +14,12 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { * Make sure you are running this command in the root of your app. * * @open Open the config file after it is created. - * @boxlang Create a .bxmigrations.json file instead of .cfmigrations.json. Defaults to auto-detection based on your server/box.json. */ function run( - boolean open = false, - boolean boxlang = isBoxLangProject( getCWD() ) + boolean open = false ) { var directory = getCWD() - var configFileName = arguments.boxlang ? ".bxmigrations.json" : ".cfmigrations.json" + var configFileName = ".cbmigrations.json" var configPath = "#directory#/#configFileName#" // Check and see if the config file already exists diff --git a/commands/migrate/install.cfc b/commands/migrate/install.cfc index 43d7f7b..291f3a2 100644 --- a/commands/migrate/install.cfc +++ b/commands/migrate/install.cfc @@ -2,9 +2,8 @@ * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp * www.ortussolutions.com * --- - * Installs the cfmigrations table in to your database. - * - * The cfmigrations table keeps track of the migrations ran against your database. + * Installs the migrations table in to your database. + * The migrations table keeps track of the migrations ran against your database. * It must be installed before running any migrations. */ component extends="commandbox-migrations.models.BaseMigrationCommand" { @@ -18,7 +17,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } diff --git a/commands/migrate/refresh.cfc b/commands/migrate/refresh.cfc index 118a03d..a470152 100644 --- a/commands/migrate/refresh.cfc +++ b/commands/migrate/refresh.cfc @@ -16,7 +16,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } diff --git a/commands/migrate/reset.cfc b/commands/migrate/reset.cfc index 5392b81..c9409c8 100644 --- a/commands/migrate/reset.cfc +++ b/commands/migrate/reset.cfc @@ -12,7 +12,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } diff --git a/commands/migrate/seed/run.cfc b/commands/migrate/seed/run.cfc index 38fce67..e0dc106 100644 --- a/commands/migrate/seed/run.cfc +++ b/commands/migrate/seed/run.cfc @@ -23,7 +23,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { } if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } @@ -42,7 +42,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { } ); } catch ( any e ) { - if ( verbose ) { + if ( arguments.verbose ) { if ( structKeyExists( e, "Sql" ) ) { print.whiteOnRedLine( "Error when trying to seed #currentlyRunningSeeder#:" ); print.line( variables.sqlHighlighter.highlight( variables.sqlFormatter.format( e.Sql ) ).toAnsi() ); diff --git a/commands/migrate/uninstall.cfc b/commands/migrate/uninstall.cfc index 7931c67..bdab0ea 100644 --- a/commands/migrate/uninstall.cfc +++ b/commands/migrate/uninstall.cfc @@ -2,10 +2,10 @@ * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp * www.ortussolutions.com * --- - * Uninstalls the cfmigrations table from your database. + * Uninstalls the migrations table from your database. * - * The cfmigrations table keeps track of the migrations ran against your database. - * Uninstall it when you are removing cfmigrations from your application. + * The migrations table keeps track of the migrations ran against your database. + * Uninstall it when you are removing migrations from your application. */ component extends="commandbox-migrations.models.BaseMigrationCommand" { @@ -13,13 +13,13 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { * @manager The Migration Manager to use. * @manager.optionsUDF completeManagers * @verbose If true, errors output a full stack trace. - * @force If true, will not wait for confirmation to uninstall cfmigrations. + * @force If true, will not wait for confirmation to uninstall cbmigrations. */ function run( string manager = "default", boolean verbose = false, boolean force = false ) { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } @@ -33,7 +33,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { if ( arguments.force || confirm( - "Uninstalling cfmigrations will also run all your migrations down. Are you sure you want to continue? [y/n]" + "Uninstalling cbmigrations will also run all your migrations down. Are you sure you want to continue? [y/n]" ) ) { variables.migrationService.uninstall(); diff --git a/commands/migrate/up.cfc b/commands/migrate/up.cfc index b1a5366..f60e034 100644 --- a/commands/migrate/up.cfc +++ b/commands/migrate/up.cfc @@ -26,7 +26,7 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); + print.blackOnYellowLine( "cbmigrations info:" ); print.line( getMigrationsInfo() ).line(); } diff --git a/models/BaseMigrationCommand.cfc b/models/BaseMigrationCommand.cfc index 25b5b12..2d9756f 100644 --- a/models/BaseMigrationCommand.cfc +++ b/models/BaseMigrationCommand.cfc @@ -56,9 +56,9 @@ component { * Registers an on-the-fly application datasource from the given connection info * and sets it as the application default, returning the datasource name. */ - function installDatasource( required struct connectionInfo, string datasourceName = "cfmigrations" ) { + function installDatasource( required struct connectionInfo, string datasourceName = "cbmigrations" ) { var datasources = getApplicationSettings().datasources ?: {}; - datasources[ "cfmigrations" ] = arguments.connectionInfo; + datasources[ "cbmigrations" ] = arguments.connectionInfo; application action='update' datasources=datasources; application action='update' datasource='#arguments.datasourceName#'; return arguments.datasourceName; @@ -98,16 +98,30 @@ component { /** * Returns the path to the first migrations config file found in the given directory, - * checking `.bxmigrations.json` before `.cfmigrations.json`, or "" if neither exists. + * checking `.cbmigrations.json` before `.cfmigrations.json`. If only + * `.cfmigrations.json` exists, the user is prompted to rename it to the new + * `.cbmigrations.json` name. */ private string function findMigrationsConfigPath( required string directory ) { - var candidates = [ ".bxmigrations.json", ".cfmigrations.json" ]; - for ( var candidate in candidates ) { - var path = "#arguments.directory#/#candidate#"; - if ( fileExists( path ) ) { - return path; + // Check for the modern config file first + var cbmigrationsPath = "#arguments.directory#/.cbmigrations.json"; + if ( fileExists( cbmigrationsPath ) ) { + return cbmigrationsPath; + } + + // Check for the legacy config file + var cfmigrationsPath = "#arguments.directory#/.cfmigrations.json"; + if ( fileExists( cfmigrationsPath ) ) { + print.boldYellowLine( "The config file .cfmigrations.json has been renamed to .cbmigrations.json in this new version of Migrations" ); + if ( confirm( "Would you like me to rename it for you? [y/n]" ) ) { + fileMove( cfmigrationsPath, cbmigrationsPath ); + print.greenLine( "Renamed .cfmigrations.json to .cbmigrations.json." ); + return cbmigrationsPath; } + print.line( "Continuing with .cfmigrations.json, but consider renaming it manually." ); + return cfmigrationsPath; } + return ""; } @@ -138,16 +152,15 @@ component { } /** - * Loads the migrations config: from `.bxmigrations.json`/`.cfmigrations.json` if present, + * Loads the migrations config: from `.cbmigrations.json`/`.cfmigrations.json` if present, * otherwise falls back to the deprecated `cfmigrations` key in box.json (auto-converting * the legacy pre-v4 format if needed). */ private struct function getMigrationsInfo() { var migrationsInfoType = "boxJSON"; - var directory = getCWD(); - // Check and see if a .bxmigrations.json or .cfmigrations.json file exists + // Check and see if a .cbmigrations.json or .cfmigrations.json file exists var configPath = findMigrationsConfigPath( directory ); if ( len( configPath ) ) { var migrationsInfo = deserializeJSON( fileRead( configPath ) ); @@ -204,15 +217,15 @@ component { } /** - * Returns "cfmigrations" if a dedicated config file or v4-style box.json config is found, + * Returns "cbmigrations" if a dedicated config file or v4-style box.json config is found, * or "boxJSON" if only the legacy pre-v4 box.json format is present. */ private string function getMigrationsConfigType() { var directory = getCWD(); - // Check and see if a .bxmigrations.json or .cfmigrations.json file exists + // Check and see if a .cbmigrations.json or .cbmigrations.json file exists if ( len( findMigrationsConfigPath( directory ) ) ) { - return "cfmigrations"; + return "cbmigrations"; } // Check and see if box.json exists @@ -224,7 +237,7 @@ component { var boxJSONMigrationsInfo = JSONService.show( boxJSON, "cfmigrations", {} ); if ( boxJSONMigrationsInfo.keyExists( "managers" ) ) { - return "cfmigrations"; + return "cbmigrations"; } return "boxJSON"; @@ -247,7 +260,7 @@ component { /** * Returns true if `word` starts with `substring` (or `substring` is empty). */ - private string function startsWith( required string word, required string substring ) { + private boolean function startsWith( required string word, required string substring ) { if ( len( arguments.substring ) == 0 ) { return true; } From c58f99c65858d082d4df4b1ee0af8f496cea2771 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 16 Jun 2026 22:59:14 +0200 Subject: [PATCH 09/14] updated agents --- AGENTS.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 38c4ed3..99d7297 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ Semicolons are **optional** in CFML/BoxLang — match the surrounding file's con Arrow functions use the fat-arrow syntax: `( migration ) => { ... }` CFML code is formatted via **CFFormat** (`.cfformat.json`). After editing `.cfc` files, run: + ``` box run-script format ``` @@ -54,7 +55,9 @@ The only automated script is `format` (CFFormat). Release flow: format → publi ## Conventions ### Copyright Header + Every `.cfc` and `.bx` file starts with: + ``` /** * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp @@ -64,7 +67,9 @@ Every `.cfc` and `.bx` file starts with: ``` ### Command Structure + All commands follow this pattern: + 1. `extends="commandbox-migrations.models.BaseMigrationCommand"` 2. JavaDoc-style `/** */` parameter annotations with `@paramName.optionsUDF completeXxx` for tab-completion wiring 3. `run()` method calls `setup( arguments.manager )` first @@ -75,6 +80,7 @@ All commands follow this pattern: 8. `pretend` mode captures SQL without executing ### Error Handling Pattern + ```js catch ( any e ) { if ( arguments.verbose ) { @@ -95,4 +101,77 @@ catch ( any e ) { | Command CFCs | Lowercase verb | `up.cfc`, `down.cfc`, `fresh.cfc` | | Models | PascalCase | `BaseMigrationCommand.cfc` | | Templates | PascalCase, `BX` suffix | `Migration.txt` / `MigrationBX.txt` | -| Config files | Dot-prefixed JSON | `.cbmigrations.json`, legacy `.cfmigrations.json` | \ No newline at end of file +| Config files | Dot-prefixed JSON | `.cbmigrations.json`, legacy `.cfmigrations.json` | + +## Skills + +Project-specific agent skills are located in `.agents/skills/`. Each skill has a `SKILL.md` file with detailed instructions. When working on a task that matches a skill's domain, read the skill file first. + +### BoxLang Language + +| Skill | Description | +|-------|-------------| +| `boxlang-application-descriptor` | Application.bx behavior: app discovery, lifecycle events, sessions, mappings, schedulers/watchers | +| `boxlang-async-programming` | BoxFuture, asyncRun, asyncAll, executors, schedulers, thread components, parallel pipelines | +| `boxlang-best-practices` | Community best practices for naming, structure, scoping, error handling, performance | +| `boxlang-cfml-migration` | Migrating from CFML (Adobe/Lucee) to BoxLang: syntax differences, bx-compat-cfml, common issues | +| `boxlang-classes-and-oop` | Classes, components, interfaces, inheritance, annotations, properties, constructors, OOP patterns | +| `boxlang-code-documenter` | Javadoc-style comments, argument/return documentation, DocBox-compatible API reference generation | +| `boxlang-code-reviewer` | Code review for quality, correctness, security, performance, and style | +| `boxlang-configuration` | boxlang.json settings, env var overrides, datasources, caches, executors, modules, logging | +| `boxlang-database-access` | queryExecute, bx:query, datasource config, parameterized queries, transactions, SQL injection prevention | +| `boxlang-docbox` | DocBox API documentation generation: install, CLI, config, output strategies, themes | +| `boxlang-file-handling` | fileRead, fileWrite, fileCopy, fileMove, directoryList, fileUpload, streaming, CSV/JSON processing | +| `boxlang-file-watchers` | Filesystem watchers: watcherNew/Start/Stop, event payloads, debounce/throttle, error thresholds | +| `boxlang-functional-programming` | Lambdas, closures, arrow functions, array/struct pipelines (map, filter, reduce), destructuring, spread | +| `boxlang-interceptors` | Interceptor/event system: registration, announcement points, pre/post hooks, BoxRegisterInterceptor | +| `boxlang-java-integration` | createObject, static methods, type conversion, importing classes, closures as functional interfaces, JARs | +| `boxlang-language-fundamentals` | Syntax, file types, variables, scopes, operators, control flow, exception handling, type system | +| `boxlang-modules-and-packages` | box install, module settings, BoxLang+ premium modules (bx-pdf, bx-redis, bx-csv), ORM, mail | +| `boxlang-runtime-cli-scripting` | CLI scripts, command-line arguments, REPL, action commands (compile, cftranspile), CLI-specific BIFs | +| `boxlang-runtime-commandbox` | Deploying via CommandBox: server.json, modules, SSL, rewrites, BoxLang+/++ subscriptions | +| `boxlang-scheduled-tasks` | Scheduler DSL, BaseScheduler/ScheduledTask APIs, cron expressions, lifecycle callbacks, bx:schedule | +| `boxlang-security` | Security review, OWASP Top 10, injection prevention, file upload safety, secrets management | +| `boxlang-templating` | .bxm templates, bx:output, bx:loop, bx:if, bx:include, bx:script, building views | +| `boxlang-testing` | TestBox: BDD specs, xUnit classes, expectations, MockBox, mockData, async testing, CLI runner | +| `boxlang-web-development` | Web apps: request/response, sessions, forms, REST APIs, HTTP clients, routing, CSRF, SSE | +| `boxlang-zip` | bx:zip component: compress, extract, filter entries, read archives, download as ZIP | + +### CommandBox CLI + +| Skill | Description | +|-------|-------------| +| `commandbox-config-settings` | Global config: set/show/clear, server defaults, ForgeBox tokens, endpoints, proxy, env overrides | +| `commandbox-deploying` | Production deployment: Docker, GitHub Actions, Heroku, Lightsail, OS service, server.json, CFConfig | +| `commandbox-developing` | Custom commands, modules, namespaces, tab completion, WireBox DI, interceptors, lifecycle events | +| `commandbox-embedded-server` | Server management: start/stop, server.json, JVM args, SSL/TLS, rewrites, rules, profiles, auth, gzip | +| `commandbox-package-management` | box.json, installing from ForgeBox/Git/HTTP, semver, dependencies, lock files, publishing | +| `commandbox-setup` | Installing/upgrading CommandBox: Homebrew, apt-get, Windows, Java requirements, first-run config | +| `commandbox-task-runners` | Task CFCs, targets, lifecycle events, interactive jobs, progress bars, async, file watching | +| `commandbox-testing` | testbox run command, runner URL, output formats, test watcher, CI integration, code coverage | +| `commandbox-usage` | CLI usage: commands, namespaces, tab completion, system settings, env vars, piping, recipes, REPL | + +### TestBox Testing + +| Skill | Description | +|-------|-------------| +| `testbox-assertions` | $assert object: isTrue, isEqual, includes, throws, between, closeTo, typeOf, custom assertions | +| `testbox-bdd` | BDD describe/it blocks, Gherkin-style suites, lifecycle hooks, focused/skipped specs, asyncAll | +| `testbox-cbmockdata` | Fake data generation: age, email, name, uuid, autoincrement, nested objects, custom suppliers | +| `testbox-expectations` | Fluent expect() matchers: toBe, toBeTrue, toHaveKey, toThrow, notToBe, chaining, custom matchers | +| `testbox-listeners` | Run listeners: onBundleStart/End, onSuiteStart/End, onSpecStart/End, progress, dashboards | +| `testbox-mockbox` | Mocks/stubs/spies: createMock, prepareMock, $args/$results/$throws, $callLog, $property, querySim | +| `testbox-reporters` | Reporters: ANTJunit, Console, Doc, JSON, JUnit, Min, Simple, XML, Streaming, custom IReporter | +| `testbox-runners` | Running tests: CLI, BoxLang CLI runner, HTML web runner, programmatic, streaming, watcher mode | +| `testbox-unit-xunit` | xUnit-style: testXxx() functions, setup/teardown lifecycle, $assert, Arrange-Act-Assert pattern | +| `testing-coverage` | Code coverage: setup, reporting, CI integration, TestBox options, interpreting metrics | +| `testing-fixtures` | Test fixtures, factory patterns, test data builders, cbMockData, fixture management | + +### Other + +| Skill | Description | +|-------|-------------| +| `ortus-coding-standards` | Official Ortus coding standards: indentation, spacing, braces, naming, alignment, comments | +| `github-action-authoring` | Composite GitHub Actions: multi-platform, PATH issues, inputs/outputs, PowerShell, CI testing | +| `java-expert` | Java services/libraries: API design, concurrency, performance, dependency management, production | +| `junit-expert` | JUnit 5 tests: lifecycle, parameterized tests, extensions, assertions, parallel execution, suites | From 214cb8d965eae02527c4d2a13b8293358838e569 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 17 Jun 2026 12:50:48 +0200 Subject: [PATCH 10/14] add tests and test harness --- .github/CODE_OF_CONDUCT.MD | 3 + .github/FUNDING.YML | 1 + .github/ISSUE_TEMPLATE/BUG_REPORT.md | 33 +++ .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md | 18 ++ .github/PULL_REQUEST_TEMPLATE.md | 29 +++ .github/SECURITY.md | 3 + .github/SUPPORT.md | 3 + .github/dependabot.yml | 19 ++ .github/workflows/tests.yml | 37 +++ .gitignore | 2 + box.json | 9 +- tests/Runner.cfc | 48 ++++ .../fixtures/multi_manager_cbmigrations.json | 34 +++ .../fixtures/valid_cbmigrations.json | 19 ++ tests/specs/BaseMigrationCommandTest.cfc | 242 ++++++++++++++++++ tests/specs/MigrateCommandsTest.cfc | 231 +++++++++++++++++ tests/specs/MigrateCreateTest.cfc | 150 +++++++++++ tests/specs/MigrateInitTest.cfc | 125 +++++++++ tests/specs/MigrateSeedCreateTest.cfc | 131 ++++++++++ tests/specs/MigrateSeedRunTest.cfc | 126 +++++++++ 20 files changed, 1261 insertions(+), 2 deletions(-) create mode 100644 .github/CODE_OF_CONDUCT.MD create mode 100644 .github/FUNDING.YML create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/SUPPORT.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/tests.yml create mode 100644 tests/Runner.cfc create mode 100644 tests/resources/fixtures/multi_manager_cbmigrations.json create mode 100644 tests/resources/fixtures/valid_cbmigrations.json create mode 100644 tests/specs/BaseMigrationCommandTest.cfc create mode 100644 tests/specs/MigrateCommandsTest.cfc create mode 100644 tests/specs/MigrateCreateTest.cfc create mode 100644 tests/specs/MigrateInitTest.cfc create mode 100644 tests/specs/MigrateSeedCreateTest.cfc create mode 100644 tests/specs/MigrateSeedRunTest.cfc diff --git a/.github/CODE_OF_CONDUCT.MD b/.github/CODE_OF_CONDUCT.MD new file mode 100644 index 0000000..12507ab --- /dev/null +++ b/.github/CODE_OF_CONDUCT.MD @@ -0,0 +1,3 @@ +# Code of Conduct + +Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#code-of-conduct). diff --git a/.github/FUNDING.YML b/.github/FUNDING.YML new file mode 100644 index 0000000..7e59d13 --- /dev/null +++ b/.github/FUNDING.YML @@ -0,0 +1 @@ +patreon: ortussolutions diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000..300232e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +--- + + + +## What are the steps to reproduce this issue? + +1. … +2. … +3. … + +## What happens? + +… + +## What were you expecting to happen? + +… + +## Any logs, error output, etc? + +… + +## Any other comments? + +… + +## What versions are you using? + +**Operating System:** … +**Package Version:** … diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000..c10946f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: Request a new feature or enhancement +--- + + + +## Summary + + + +## Detailed Description + + + +## Possible Implementation Ideas + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e8bd9f9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ +# Description + +Please include a summary of the changes and which issue(s) is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +**Please note that all PRs must have tests attached to them** + +IMPORTANT: Please review the [CONTRIBUTING.md](../CONTRIBUTING.md) file for detailed contributing guidelines. + +## Issues + +All PRs must have an accompanied issue. Please make sure you created it and linked it here. + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug Fix +- [ ] Improvement +- [ ] New Feature +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## Checklist + +- [ ] My code follows the style guidelines of this project [cfformat](../.cfformat.json) +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..f057099 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#security-vulnerabilities). diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..3bb8adb --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,3 @@ +# Support & Help + +Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#support-questions). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e4043a8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + # GitHub Actions - updates uses: statements in workflows + - package-ecosystem: "github-actions" + directory: "/" # Where your .github/workflows/ folder is + schedule: + interval: "quarterly" + + # Gradle - updates dependencies in build.gradle or build.gradle.kts + - package-ecosystem: "gradle" + directory: "/" # Adjust if build.gradle is in a subfolder + schedule: + interval: "quarterly" + + # NPM + - package-ecosystem: "npm" + directory: "/" # adjust if needed + schedule: + interval: "quarterly" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..11a8019 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +name: Tests + +on: + push: + branches: + - master + - development + - "feature/**" + - "bugfix/**" + - "hotfix/**" + - "release/**" + - "claude/**" + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: TestBox Suite + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup CommandBox + uses: ortus-boxlang/setup-boxlang@main + with: + with-commandbox: true + + - name: Install dependencies + run: box install + + - name: Run tests + run: box run-script test diff --git a/.gitignore b/.gitignore index b2b8fe3..ad94da0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /modules/ +/testbox/ +.test-tmp/ TASKS.md jmimemagic.log diff --git a/box.json b/box.json index c1278fa..0feccab 100644 --- a/box.json +++ b/box.json @@ -23,7 +23,8 @@ "scripts":{ "onRelease":"publish", "postPublish":"!git push && git push --tags", - "format":"cfformat run commands/**/*.cfc,ModuleConfig.cfc --overwrite" + "format":"cfformat run commands/**/*.cfc,ModuleConfig.cfc --overwrite", + "test":"task run taskFile=tests/Runner.cfc" }, "private":false, "license":[ @@ -36,8 +37,12 @@ "cfmigrations":"^5.1.0", "sqlformatter":"^1.1.3+31" }, + "devDependencies":{ + "testbox":"*" + }, "installPaths":{ "cfmigrations":"modules/cfmigrations/", - "sqlformatter":"modules/sqlformatter/" + "sqlformatter":"modules/sqlformatter/", + "testbox":"testbox/" } } diff --git a/tests/Runner.cfc b/tests/Runner.cfc new file mode 100644 index 0000000..f1c9826 --- /dev/null +++ b/tests/Runner.cfc @@ -0,0 +1,48 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * TestBox Test Runner for commandbox-migrations + * + * Usage: + * box run-script test + * box task run taskFile=tests/Runner.cfc + */ +component { + + /** + * Main test runner + */ + function run() { + var projectRoot = resolvePath( "../" ) + var testsDir = projectRoot & "tests/" + + // Create filesystem mappings for the test specs to access the command and template files + variables.fileSystemUtil.createMapping( "/testbox", projectRoot & "testbox" ) + variables.fileSystemUtil.createMapping( "/tests", testsDir ) + variables.fileSystemUtil.createMapping( "/commandbox-migrations", projectRoot ) + + // Seed the test specs with the project root so they can locate the command files and templates + request.commandboxMigrationsProjectRoot = projectRoot + + var tb = new testbox.system.TestBox( + directory = { "mapping" : "tests.specs", "recurse" : false }, + reporter = "console" + ) + + print.line( tb.run() ).toConsole() + + var testResult = tb.getResult() + var totalFail = testResult.getTotalFail() + var totalError = testResult.getTotalError() + + if ( testResult.getTotalSpecs() == 0 ) { + error( "No test specs were executed" ) + } + + if ( totalFail > 0 || totalError != 0 ) { + error( "Test suite failed with #totalFail# failures and #totalError# errors" ) + } + } + +} diff --git a/tests/resources/fixtures/multi_manager_cbmigrations.json b/tests/resources/fixtures/multi_manager_cbmigrations.json new file mode 100644 index 0000000..2e9d5f4 --- /dev/null +++ b/tests/resources/fixtures/multi_manager_cbmigrations.json @@ -0,0 +1,34 @@ +{ + "default": { + "manager": "cfmigrations.models.QBMigrationManager", + "migrationsDirectory": "resources/database/migrations/", + "seedsDirectory": "resources/database/seeds/", + "properties": { + "defaultGrammar": "AutoDiscover@qb", + "connectionInfo": { + "type": "mysql", + "database": "primarydb", + "host": "127.0.0.1", + "port": 3306, + "username": "root", + "password": "" + } + } + }, + "secondary": { + "manager": "cfmigrations.models.QBMigrationManager", + "migrationsDirectory": "resources/database/secondary-migrations/", + "seedsDirectory": "resources/database/secondary-seeds/", + "properties": { + "defaultGrammar": "AutoDiscover@qb", + "connectionInfo": { + "type": "postgresql", + "database": "secondarydb", + "host": "127.0.0.1", + "port": 5432, + "username": "postgres", + "password": "" + } + } + } +} diff --git a/tests/resources/fixtures/valid_cbmigrations.json b/tests/resources/fixtures/valid_cbmigrations.json new file mode 100644 index 0000000..f1d62e8 --- /dev/null +++ b/tests/resources/fixtures/valid_cbmigrations.json @@ -0,0 +1,19 @@ +{ + "default": { + "manager": "cfmigrations.models.QBMigrationManager", + "migrationsDirectory": "resources/database/migrations/", + "seedsDirectory": "resources/database/seeds/", + "properties": { + "defaultGrammar": "AutoDiscover@qb", + "connectionInfo": { + "type": "mysql", + "database": "testdb", + "host": "127.0.0.1", + "port": 3306, + "username": "root", + "password": "", + "class": "com.mysql.cj.jdbc.Driver" + } + } + } +} diff --git a/tests/specs/BaseMigrationCommandTest.cfc b/tests/specs/BaseMigrationCommandTest.cfc new file mode 100644 index 0000000..ae9adb5 --- /dev/null +++ b/tests/specs/BaseMigrationCommandTest.cfc @@ -0,0 +1,242 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Tests for the commandbox-migrations configuration templates and fixtures. + * These tests validate the template files, generated fixtures, and expected + * configuration structures without requiring a live database or running server. + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll() { + variables.projectRoot = request.commandboxMigrationsProjectRoot; + } + + function run() { + + describe( "Migration Config Templates", () => { + + describe( ".cbmigrations.json template", () => { + + it( "should exist as config.txt", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + expect( fileExists( templatePath ) ).toBeTrue(); + }); + + it( "should contain valid JSON", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var templateContent = replace( fileRead( templatePath ), "$" & "{DB_PORT}", "5432" ); + var config = deserializeJSON( templateContent ); + expect( isStruct( config ) ).toBeTrue(); + }); + + it( "should contain a default manager entry", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var config = deserializeJSON( replace( fileRead( templatePath ), "$" & "{DB_PORT}", "5432" ) ); + expect( config ).toHaveKey( "default" ); + }); + + it( "should use QBMigrationManager as the default manager", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var config = deserializeJSON( replace( fileRead( templatePath ), "$" & "{DB_PORT}", "5432" ) ); + expect( config.default ).toHaveKey( "manager" ); + expect( config.default.manager ).toInclude( "QBMigrationManager" ); + }); + + it( "should define a migrationsDirectory", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var config = deserializeJSON( replace( fileRead( templatePath ), "$" & "{DB_PORT}", "5432" ) ); + expect( config.default ).toHaveKey( "migrationsDirectory" ); + expect( config.default.migrationsDirectory ).notToBeEmpty(); + }); + + it( "should define a seedsDirectory", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var config = deserializeJSON( replace( fileRead( templatePath ), "$" & "{DB_PORT}", "5432" ) ); + expect( config.default ).toHaveKey( "seedsDirectory" ); + expect( config.default.seedsDirectory ).notToBeEmpty(); + }); + + it( "should define connectionInfo with database driver placeholder", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var config = deserializeJSON( replace( fileRead( templatePath ), "$" & "{DB_PORT}", "5432" ) ); + expect( config.default ).toHaveKey( "properties" ); + expect( config.default.properties ).toHaveKey( "connectionInfo" ); + expect( config.default.properties.connectionInfo ).toHaveKey( "type" ); + }); + + it( "should use environment variables for connection info", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var templateContent = fileRead( templatePath ); + expect( templateContent ).toInclude( "${DB_DRIVER}" ); + expect( templateContent ).toInclude( "${DB_DATABASE}" ); + expect( templateContent ).toInclude( "${DB_HOST}" ); + }); + + }); + + describe( "Migration templates", () => { + + it( "should have a CFML migration template", () => { + expect( fileExists( variables.projectRoot & "templates/Migration.txt" ) ).toBeTrue(); + }); + + it( "should have a BoxLang migration template", () => { + expect( fileExists( variables.projectRoot & "templates/MigrationBX.txt" ) ).toBeTrue(); + }); + + it( "CFML template should use component keyword", () => { + var content = fileRead( variables.projectRoot & "templates/Migration.txt" ); + expect( content ).toInclude( "component" ); + }); + + it( "BoxLang template should use class keyword", () => { + var content = fileRead( variables.projectRoot & "templates/MigrationBX.txt" ); + expect( content ).toInclude( "class" ); + }); + + it( "both templates should define up() and down() functions", () => { + var cfmlContent = fileRead( variables.projectRoot & "templates/Migration.txt" ); + var bxContent = fileRead( variables.projectRoot & "templates/MigrationBX.txt" ); + expect( cfmlContent ).toInclude( "function up(" ); + expect( cfmlContent ).toInclude( "function down(" ); + expect( bxContent ).toInclude( "function up(" ); + expect( bxContent ).toInclude( "function down(" ); + }); + + it( "both templates should accept schema and qb parameters", () => { + var cfmlContent = fileRead( variables.projectRoot & "templates/Migration.txt" ); + var bxContent = fileRead( variables.projectRoot & "templates/MigrationBX.txt" ); + expect( cfmlContent ).toInclude( "schema" ); + expect( cfmlContent ).toInclude( "qb" ); + expect( bxContent ).toInclude( "schema" ); + expect( bxContent ).toInclude( "qb" ); + }); + + }); + + describe( "Seed templates", () => { + + it( "should have a CFML seed template", () => { + expect( fileExists( variables.projectRoot & "templates/seed.txt" ) ).toBeTrue(); + }); + + it( "should have a BoxLang seed template", () => { + expect( fileExists( variables.projectRoot & "templates/seedBX.txt" ) ).toBeTrue(); + }); + + it( "CFML seed template should use component keyword", () => { + var content = fileRead( variables.projectRoot & "templates/seed.txt" ); + expect( content ).toInclude( "component" ); + }); + + it( "BoxLang seed template should use class keyword", () => { + var content = fileRead( variables.projectRoot & "templates/seedBX.txt" ); + expect( content ).toInclude( "class" ); + }); + + it( "both seed templates should define a run() function", () => { + var cfmlContent = fileRead( variables.projectRoot & "templates/seed.txt" ); + var bxContent = fileRead( variables.projectRoot & "templates/seedBX.txt" ); + expect( cfmlContent ).toInclude( "function run(" ); + expect( bxContent ).toInclude( "function run(" ); + }); + + }); + + }); + + describe( "Config Fixtures", () => { + + beforeEach( () => { + variables.fixturesDir = variables.projectRoot & "tests/resources/fixtures/"; + if ( !directoryExists( variables.fixturesDir ) ) { + directoryCreate( variables.fixturesDir ); + } + }); + + describe( "Valid .cbmigrations.json fixture", () => { + + it( "should exist", () => { + var fixturePath = variables.fixturesDir & "valid_cbmigrations.json"; + expect( fileExists( fixturePath ) ).toBeTrue(); + }); + + it( "should contain valid JSON", () => { + var fixturePath = variables.fixturesDir & "valid_cbmigrations.json"; + var config = deserializeJSON( fileRead( fixturePath ) ); + expect( isStruct( config ) ).toBeTrue(); + }); + + it( "should define a default manager", () => { + var fixturePath = variables.fixturesDir & "valid_cbmigrations.json"; + var config = deserializeJSON( fileRead( fixturePath ) ); + expect( config ).toHaveKey( "default" ); + expect( config.default ).toHaveKey( "manager" ); + }); + + }); + + describe( "Multiple managers fixture", () => { + + it( "should exist", () => { + var fixturePath = variables.fixturesDir & "multi_manager_cbmigrations.json"; + expect( fileExists( fixturePath ) ).toBeTrue(); + }); + + it( "should define multiple managers", () => { + var fixturePath = variables.fixturesDir & "multi_manager_cbmigrations.json"; + var config = deserializeJSON( fileRead( fixturePath ) ); + expect( structCount( config ) ).toBeGTE( 2 ); + }); + + it( "should define default and secondary managers", () => { + var fixturePath = variables.fixturesDir & "multi_manager_cbmigrations.json"; + var config = deserializeJSON( fileRead( fixturePath ) ); + expect( config ).toHaveKey( "default" ); + expect( config ).toHaveKey( "secondary" ); + }); + + }); + + }); + + describe( "Configuration File Patterns", () => { + + it( "should recognize .cbmigrations.json as the preferred config format", () => { + // Document the resolution order: .cbmigrations.json > .cfmigrations.json + var preferredConfig = ".cbmigrations.json"; + var legacyConfig = ".cfmigrations.json"; + expect( preferredConfig ).notToBe( legacyConfig ); + // The actual resolution logic is in findMigrationsConfigPath() + // which can only be tested with mocked dependencies + }); + + it( "should support legacy .cfmigrations.json format", () => { + // Document: legacy format is still supported but deprecated + var legacyFormat = ".cfmigrations.json"; + expect( legacyFormat ).toInclude( "cfmigrations" ); + }); + + it( "should support box.json as fallback configuration", () => { + // Document: box.json with cfmigrations key is deprecated but supported + var boxJsonConfig = { + "name" : "test-project", + "cfmigrations" : { + "managers" : { + "default" : { + "manager" : "cfmigrations.models.QBMigrationManager", + "migrationsDirectory" : "resources/database/migrations/" + } + } + } + }; + expect( boxJsonConfig ).toHaveKey( "cfmigrations" ); + expect( boxJsonConfig.cfmigrations ).toHaveKey( "managers" ); + }); + + }); + + } + +} diff --git a/tests/specs/MigrateCommandsTest.cfc b/tests/specs/MigrateCommandsTest.cfc new file mode 100644 index 0000000..7b9bc51 --- /dev/null +++ b/tests/specs/MigrateCommandsTest.cfc @@ -0,0 +1,231 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Tests for the migrate install/up/down/reset/fresh command structure and behavior patterns. + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll() { + variables.projectRoot = request.commandboxMigrationsProjectRoot; + } + + function run() { + + describe( "migrate install command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/install.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/install.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should call setup() before installation", () => { + var commandPath = variables.projectRoot & "commands/migrate/install.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "setup(" ); + } ); + + it( "should check if migration service is ready before installing", () => { + var commandPath = variables.projectRoot & "commands/migrate/install.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "isReady" ); + } ); + + it( "should call migrationService.install()", () => { + var commandPath = variables.projectRoot & "commands/migrate/install.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "migrationService.install()" ); + } ); + + } ); + + describe( "migrate up command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should support once parameter for single migration", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean once" ); + } ); + + it( "should support pretend parameter for dry-run mode", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean pretend" ); + } ); + + it( "should support file parameter for specific migration", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string file" ); + } ); + + it( "should support seed parameter to run seeders", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean seed" ); + } ); + + it( "should use hooks for pre/post migration logging", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "preProcessHook" ); + expect( content ).toInclude( "postProcessHook" ); + } ); + + it( "should clear page pool before execution", () => { + var commandPath = variables.projectRoot & "commands/migrate/up.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "pagePoolClear" ); + } ); + + } ); + + describe( "migrate down command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/down.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/down.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should support file parameter for specific rollback", () => { + var commandPath = variables.projectRoot & "commands/migrate/down.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string file" ); + } ); + + it( "should support pretend parameter for dry-run mode", () => { + var commandPath = variables.projectRoot & "commands/migrate/down.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean pretend" ); + } ); + + it( "should roll back the last batch when no file specified", () => { + var commandPath = variables.projectRoot & "commands/migrate/down.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "runAllMigrations" ); + expect( content ).toInclude( 'direction = "down"' ); + } ); + + } ); + + describe( "migrate reset command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/reset.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/reset.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should delegate reset behavior to the migration service", () => { + var commandPath = variables.projectRoot & "commands/migrate/reset.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "migrationService.reset()" ); + } ); + + } ); + + describe( "migrate fresh command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/fresh.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/fresh.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should chain reset, install, and up commands", () => { + var commandPath = variables.projectRoot & "commands/migrate/fresh.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "command(" ) + // Should call at least migrate reset, migrate install, and migrate up + } ); + + it( "should use .run() to execute chained commands", () => { + var commandPath = variables.projectRoot & "commands/migrate/fresh.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( ".run()" ); + } ); + + } ); + + describe( "Common error handling patterns", () => { + + it( "commands with direct migration service calls should have try/catch blocks", () => { + var commands = [ "install.cfc", "up.cfc", "down.cfc", "reset.cfc" ]; + for ( var cmd in commands ) { + var commandPath = variables.projectRoot & "commands/migrate/#cmd#"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "try {" ); + expect( content ).toInclude( "catch" ); + } + } ); + + it( "commands with direct migration service calls should format SQL on errors", () => { + var commands = [ "install.cfc", "up.cfc", "down.cfc", "reset.cfc" ]; + for ( var cmd in commands ) { + var commandPath = variables.projectRoot & "commands/migrate/#cmd#"; + var content = fileRead( commandPath ); + // Should reference sqlHighlighter or sqlFormatter in catch blocks + expect( content ).toInclude( "sqlHighlighter" ); + } + } ); + + it( "commands with direct migration service calls should use error() to propagate exit codes", () => { + var commands = [ "install.cfc", "up.cfc", "down.cfc", "reset.cfc" ]; + for ( var cmd in commands ) { + var commandPath = variables.projectRoot & "commands/migrate/#cmd#"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "error(" ); + } + } ); + + it( "all commands should support verbose parameter for diagnostics", () => { + var commands = [ "install.cfc", "up.cfc", "down.cfc", "reset.cfc", "fresh.cfc" ]; + for ( var cmd in commands ) { + var commandPath = variables.projectRoot & "commands/migrate/#cmd#"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean verbose" ); + } + } ); + + } ); + } + +} diff --git a/tests/specs/MigrateCreateTest.cfc b/tests/specs/MigrateCreateTest.cfc new file mode 100644 index 0000000..dababf2 --- /dev/null +++ b/tests/specs/MigrateCreateTest.cfc @@ -0,0 +1,150 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Tests for the migrate create command structure, template selection, + * and timestamp generation patterns. + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll() { + variables.projectRoot = request.commandboxMigrationsProjectRoot; + } + + function run() { + + describe( "migrate create command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should define a run() method", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "function run(" ); + } ); + + it( "should accept a name parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string name" ); + } ); + + it( "should accept a manager parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string manager" ); + } ); + + it( "should accept a boxlang boolean parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean boxlang" ); + } ); + + it( "should call setup() with setupDatasource=false", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "setup(" ).toInclude( "setupDatasource" ); + } ); + + it( "should choose the CFML migration template when boxlang is false", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "Migration##arguments.boxlang" ); + expect( content ).toInclude( '? "BX" : ""' ); + } ); + + it( "should choose the BoxLang migration template when boxlang is true", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "Migration##arguments.boxlang" ); + expect( content ).toInclude( '? "BX" : ""' ); + } ); + + it( "should use the yyyy_mm_dd_HHnnss timestamp format", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "yyyy_mm_dd_HHnnss" ); + } ); + + it( "should create the migrationsDirectory if it does not exist", () => { + var commandPath = variables.projectRoot & "commands/migrate/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "directoryCreate" ); + } ); + + } ); + + describe( "migrate create migration templates", () => { + + it( "should have a CFML migration template", () => { + var templatePath = variables.projectRoot & "templates/Migration.txt"; + expect( fileExists( templatePath ) ).toBeTrue(); + } ); + + it( "should have a BoxLang migration template", () => { + var templatePath = variables.projectRoot & "templates/MigrationBX.txt"; + expect( fileExists( templatePath ) ).toBeTrue(); + } ); + + it( "CFML template should use component keyword", () => { + var content = fileRead( variables.projectRoot & "templates/Migration.txt" ); + expect( content ).toInclude( "component" ); + } ); + + it( "BoxLang template should use class keyword", () => { + var content = fileRead( variables.projectRoot & "templates/MigrationBX.txt" ); + expect( content ).toInclude( "class" ); + } ); + + it( "CFML template should define up() and down() methods", () => { + var content = fileRead( variables.projectRoot & "templates/Migration.txt" ); + expect( content ).toInclude( "function up(" ); + expect( content ).toInclude( "function down(" ); + } ); + + it( "BoxLang template should define up() and down() methods", () => { + var content = fileRead( variables.projectRoot & "templates/MigrationBX.txt" ); + expect( content ).toInclude( "function up(" ); + expect( content ).toInclude( "function down(" ); + } ); + + it( "Both templates should accept schema and qb arguments", () => { + var cfmlContent = fileRead( variables.projectRoot & "templates/Migration.txt" ); + var bxContent = fileRead( variables.projectRoot & "templates/MigrationBX.txt" ); + expect( cfmlContent ).toInclude( "schema" ); + expect( cfmlContent ).toInclude( "qb" ); + expect( bxContent ).toInclude( "schema" ); + expect( bxContent ).toInclude( "qb" ); + } ); + + } ); + + describe( "migrate create timestamp format", () => { + + it( "should generate a valid timestamp string", () => { + // Simulate what the command does + var timestamp = dateFormat( now(), "yyyy_mm_dd" ) & "_" & timeFormat( now(), "HHnnss" ); + expect( reFind( "^\d{4}_\d{2}_\d{2}_\d{6}$", timestamp ) ).toBeTrue(); + } ); + + it( "should produce sortable filenames", () => { + var ts1 = dateFormat( "2024-01-01", "yyyy_mm_dd" ) & "_" & timeFormat( "08:00:00", "HHnnss" ); + var ts2 = dateFormat( "2024-01-02", "yyyy_mm_dd" ) & "_" & timeFormat( "08:00:00", "HHnnss" ); + expect( ts1 < ts2 ).toBeTrue(); + } ); + + } ); + } + +} diff --git a/tests/specs/MigrateInitTest.cfc b/tests/specs/MigrateInitTest.cfc new file mode 100644 index 0000000..229b2fb --- /dev/null +++ b/tests/specs/MigrateInitTest.cfc @@ -0,0 +1,125 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Tests for the migrate init command structure, template usage, + * and configuration generation patterns. + * + * Since CommandBox commands require shell context to execute, + * these tests validate the command's file structure, template + * resolution, and expected configuration output. + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll() { + variables.projectRoot = request.commandboxMigrationsProjectRoot; + } + + function run() { + + describe( "migrate init command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should define a run() method", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "function run(" ); + } ); + + it( "should accept an open parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean open" ); + } ); + + it( "should reference the config.txt template", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "config.txt" ); + } ); + + it( "should check if .cbmigrations.json already exists before creating", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "fileExists" ); + expect( content ).toInclude( ".cbmigrations.json" ); + } ); + + it( "should write the config file with mode 777", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "mode=" ); + expect( content ).toInclude( "777" ); + } ); + + } ); + + describe( "init command config template output", () => { + + it( "should produce valid JSON when config.txt is written", () => { + var templatePath = variables.projectRoot & "templates/config.txt"; + var content = replace( fileRead( templatePath ), "$" & "{DB_PORT}", "5432" ); + // trim() is applied in the command before writing + var config = deserializeJSON( trim( content ) ); + expect( isStruct( config ) ).toBeTrue(); + } ); + + it( "should produce a config with the correct default manager", () => { + var content = replace( fileRead( variables.projectRoot & "templates/config.txt" ), "$" & "{DB_PORT}", "5432" ); + var config = deserializeJSON( trim( content ) ); + expect( config.default.manager ).toBe( "cfmigrations.models.QBMigrationManager" ); + } ); + + it( "should produce a config with environment variable placeholders", () => { + var content = fileRead( variables.projectRoot & "templates/config.txt" ); + expect( content ).toInclude( "${DB_DRIVER}" ); + expect( content ).toInclude( "${DB_DATABASE}" ); + expect( content ).toInclude( "${DB_HOST}" ); + } ); + + it( "should produce a config file under 500 characters after trimming", () => { + // The config template should be compact enough for a config file + var content = trim( fileRead( variables.projectRoot & "templates/config.txt" ) ); + expect( len( content ) ).toBeGT( 100 ); + expect( len( content ) ).toBeLT( 2000 ); + } ); + + } ); + + describe( "init command idempotency logic", () => { + + it( "should use fileExists() to detect existing configs", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "fileExists( configPath )" ); + } ); + + it( "should return early when config already exists", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + // Should have a return statement within the fileExists check + expect( content ).toInclude( "return" ); + } ); + + it( "should print a yellow warning when config already exists", () => { + var commandPath = variables.projectRoot & "commands/migrate/init.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "print.yellowLine" ); + expect( content ).toInclude( "already exists" ); + } ); + + } ); + } + +} diff --git a/tests/specs/MigrateSeedCreateTest.cfc b/tests/specs/MigrateSeedCreateTest.cfc new file mode 100644 index 0000000..31c8bab --- /dev/null +++ b/tests/specs/MigrateSeedCreateTest.cfc @@ -0,0 +1,131 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Tests for the migrate seed create command structure and template selection. + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll() { + variables.projectRoot = request.commandboxMigrationsProjectRoot; + } + + function run() { + + describe( "migrate seed create command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should define a run() method", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "function run(" ); + } ); + + it( "should accept a name parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string name" ); + } ); + + it( "should accept a manager parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string manager" ); + } ); + + it( "should accept a boxlang boolean parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean boxlang" ); + } ); + + it( "should call setup() with setupDatasource=false", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "setup(" ).toInclude( "setupDatasource" ); + } ); + + it( "should choose the CFML seed template when boxlang is false", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "seed##arguments.boxlang" ); + expect( content ).toInclude( '? "BX" : ""' ); + } ); + + it( "should choose the BoxLang seed template when boxlang is true", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "seed##arguments.boxlang" ); + expect( content ).toInclude( '? "BX" : ""' ); + } ); + + it( "should use the provided seed name without a timestamp", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "##arguments.name##.##extension##" ); + } ); + + it( "should create the seedsDirectory if it does not exist", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/create.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "directoryCreate" ); + } ); + + } ); + + describe( "migrate seed create templates", () => { + + it( "should have a CFML seed template", () => { + var templatePath = variables.projectRoot & "templates/seed.txt"; + expect( fileExists( templatePath ) ).toBeTrue(); + } ); + + it( "should have a BoxLang seed template", () => { + var templatePath = variables.projectRoot & "templates/seedBX.txt"; + expect( fileExists( templatePath ) ).toBeTrue(); + } ); + + it( "CFML template should use component keyword", () => { + var content = fileRead( variables.projectRoot & "templates/seed.txt" ); + expect( content ).toInclude( "component" ); + } ); + + it( "BoxLang template should use class keyword", () => { + var content = fileRead( variables.projectRoot & "templates/seedBX.txt" ); + expect( content ).toInclude( "class" ); + } ); + + it( "CFML template should define a run() method", () => { + var content = fileRead( variables.projectRoot & "templates/seed.txt" ); + expect( content ).toInclude( "function run(" ); + } ); + + it( "BoxLang template should define a run() method", () => { + var content = fileRead( variables.projectRoot & "templates/seedBX.txt" ); + expect( content ).toInclude( "function run(" ); + } ); + + it( "Both templates should accept qb and mockdata arguments", () => { + var cfmlContent = fileRead( variables.projectRoot & "templates/seed.txt" ); + var bxContent = fileRead( variables.projectRoot & "templates/seedBX.txt" ); + expect( cfmlContent ).toInclude( "qb" ); + expect( cfmlContent ).toInclude( "mockdata" ); + expect( bxContent ).toInclude( "qb" ); + expect( bxContent ).toInclude( "mockdata" ); + } ); + + } ); + } + +} diff --git a/tests/specs/MigrateSeedRunTest.cfc b/tests/specs/MigrateSeedRunTest.cfc new file mode 100644 index 0000000..c6d1324 --- /dev/null +++ b/tests/specs/MigrateSeedRunTest.cfc @@ -0,0 +1,126 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Tests for the migrate seed run command structure and execution patterns. + */ +component extends="testbox.system.BaseSpec" { + + function beforeAll() { + variables.projectRoot = request.commandboxMigrationsProjectRoot; + } + + function run() { + + describe( "migrate seed run command structure", () => { + + it( "should be a valid CFC file", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + expect( fileExists( commandPath ) ).toBeTrue(); + } ); + + it( "should extend BaseMigrationCommand", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "extends=" ) + expect( content ).toInclude( "BaseMigrationCommand" ); + } ); + + it( "should define a run() method", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "function run(" ); + } ); + + it( "should accept a manager parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string manager" ); + } ); + + it( "should accept a name parameter for a specific seeder", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "string name" ); + } ); + + it( "should accept a verbose boolean parameter", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "boolean verbose" ); + } ); + + it( "should call setup() before execution", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "setup(" ); + } ); + + it( "should clear page pool before seed execution", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "pagePoolClear" ); + } ); + + it( "should use preProcessHook for logging", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "preProcessHook" ); + } ); + + it( "should use postProcessHook for logging", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "postProcessHook" ); + } ); + + } ); + + describe( "migrate seed run error handling", () => { + + it( "should have try/catch blocks", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "try {" ); + expect( content ).toInclude( "catch" ); + } ); + + it( "should format SQL on errors", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "sqlHighlighter" ); + } ); + + it( "should use error() to propagate exit codes", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "error(" ); + } ); + + it( "should check SQL existence in error structure", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "structKeyExists" ); + } ); + + } ); + + describe( "migrate seed run seeder selection", () => { + + it( "should pass the selected seed name to the migration service", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "seedName" ); + expect( content ).toInclude( "arguments.name" ); + } ); + + it( "should report when no seeders run", () => { + var commandPath = variables.projectRoot & "commands/migrate/seed/run.cfc"; + var content = fileRead( commandPath ); + expect( content ).toInclude( "No seeders to run" ); + } ); + + } ); + } + +} From dc57ded970c98af89e5e9287fcb062f9b1c0a0c9 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 17 Jun 2026 13:03:51 +0200 Subject: [PATCH 11/14] tests --- tests/Runner.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Runner.cfc b/tests/Runner.cfc index f1c9826..0e4e160 100644 --- a/tests/Runner.cfc +++ b/tests/Runner.cfc @@ -27,7 +27,7 @@ component { var tb = new testbox.system.TestBox( directory = { "mapping" : "tests.specs", "recurse" : false }, - reporter = "console" + reporter = "text" ) print.line( tb.run() ).toConsole() From 3ce6a85230d1c567f2fd2a3f8be993e271f96fc1 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 17 Jun 2026 13:05:34 +0200 Subject: [PATCH 12/14] more fine tuning --- .github/workflows/tests.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 11a8019..76afdb6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,11 +5,6 @@ on: branches: - master - development - - "feature/**" - - "bugfix/**" - - "hotfix/**" - - "release/**" - - "claude/**" pull_request: workflow_dispatch: From 3f601f25334bad2d4f721d92335a79bfa930f6bd Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 19 Jun 2026 15:31:31 +0200 Subject: [PATCH 13/14] tons of help docs --- commands/migrate/create.cfc | 17 ++++++-- commands/migrate/down.cfc | 26 ++++++++++-- commands/migrate/fresh.cfc | 25 +++++++++-- commands/migrate/help.cfc | 72 ++++++++++++++++++++++++++++++++ commands/migrate/init.cfc | 24 +++++++---- commands/migrate/install.cfc | 25 ++++++++--- commands/migrate/refresh.cfc | 21 ++++++++-- commands/migrate/reset.cfc | 19 ++++++++- commands/migrate/seed/create.cfc | 23 +++++++++- commands/migrate/seed/help.cfc | 48 +++++++++++++++++++++ commands/migrate/seed/run.cfc | 25 +++++++++-- commands/migrate/uninstall.cfc | 28 ++++++++++--- commands/migrate/up.cfc | 29 +++++++++++-- 13 files changed, 342 insertions(+), 40 deletions(-) create mode 100644 commands/migrate/help.cfc create mode 100644 commands/migrate/seed/help.cfc diff --git a/commands/migrate/create.cfc b/commands/migrate/create.cfc index 5e66a8d..baa30af 100644 --- a/commands/migrate/create.cfc +++ b/commands/migrate/create.cfc @@ -1,12 +1,23 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- * Create a new migration CFC in an existing application. * Make sure you are running this command in the root of your app. * * It prepends the date at the beginning of the file name so * you can keep your migrations in the correct order. + * + * {code:bash} + * ## Create a simple migration + * migrate create CreateUsersTable + * + * ## Create and immediately open the migration for editing + * migrate create AddEmailToUsers --open + * + * ## Create a BoxLang migration (.bx) + * migrate create CreateProductsTable --boxlang + * + * ## Create a migration for a named manager + * migrate create CreateOrdersTable --manager=secondary + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/down.cfc b/commands/migrate/down.cfc index 940763b..19ed35b 100644 --- a/commands/migrate/down.cfc +++ b/commands/migrate/down.cfc @@ -1,8 +1,28 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- * Rollback one or all of the migrations already ran against your database. + * + * Migrations are rolled back in reverse chronological order (newest first). + * Each migration's `down()` method is called to reverse the changes. + * + * {code:bash} + * ## Roll back all applied migrations + * migrate down + * + * ## Roll back only the last applied migration + * migrate down --once + * + * ## Preview rollback SQL without executing + * migrate down --pretend + * + * ## Save the pretend SQL output to a file + * migrate down --pretend --file=rollback.sql + * + * ## Roll back migrations for a named manager + * migrate down --manager=secondary + * + * ## Roll back with verbose error output + * migrate down --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/fresh.cfc b/commands/migrate/fresh.cfc index 0096284..f6b78a6 100644 --- a/commands/migrate/fresh.cfc +++ b/commands/migrate/fresh.cfc @@ -1,8 +1,25 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- - * Resets the database and runs all migrations up. + * Drop all database objects and re-run every migration from scratch. + * + * WARNING: This is a destructive operation! It calls `migrate reset` to wipe + * the entire database schema, then re-installs the migrations table and applies + * all migrations in order. All data will be lost. + * + * Use this command to get a clean-slate database during development. + * + * {code:bash} + * ## Drop everything and re-run all migrations + * migrate fresh + * + * ## Drop everything, re-run migrations, then seed the database + * migrate fresh --seed + * + * ## Run a fresh migration for a named manager + * migrate fresh --manager=secondary + * + * ## Run with verbose error output + * migrate fresh --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/help.cfc b/commands/migrate/help.cfc new file mode 100644 index 0000000..74009f5 --- /dev/null +++ b/commands/migrate/help.cfc @@ -0,0 +1,72 @@ +/** + * Display help and usage information for the migrate commands. + */ +component excludeFromHelp=true extends="commandbox-migrations.models.BaseMigrationCommand" { + + function run(){ + print + .line() + .boldCyan( "CommandBox Migrations (cbmigrations)" ) + .line() + .line() + .whiteLine( "Manage your database schema and data using versioned migration files." ) + .whiteLine( "All migrations are tracked in a migrations table so every change is recorded." ) + .line() + .boldWhiteLine( "Setup:" ) + .line() + .greenLine( " migrate init Initialize your project with a .cbmigrations.json config file" ) + .greenLine( " migrate install Install the migrations tracking table in your database" ) + .greenLine( " migrate uninstall Remove the migrations tracking table from your database" ) + .line() + .boldWhiteLine( "Running Migrations:" ) + .line() + .greenLine( " migrate up Apply one or all pending migrations" ) + .greenLine( " migrate down Rollback one or all applied migrations" ) + .greenLine( " migrate fresh Drop all objects and re-run all migrations (destructive!)" ) + .greenLine( " migrate refresh Rollback all migrations then re-run them (reset + up)" ) + .greenLine( " migrate reset Reset the database by clearing all objects" ) + .line() + .boldWhiteLine( "Scaffolding:" ) + .line() + .greenLine( " migrate create Create a new migration file" ) + .line() + .boldWhiteLine( "Seeders:" ) + .line() + .greenLine( " migrate seed run Run one or all database seeders" ) + .greenLine( " migrate seed create Create a new seeder file" ) + .line() + .yellowLine( "Examples:" ) + .line() + .dim( " ## Initialize migrations for a new project" ) + .line( " migrate init" ) + .line() + .dim( " ## Install the migrations table, then run all migrations" ) + .line( " migrate install && migrate up" ) + .line() + .dim( " ## Create and immediately open a new migration" ) + .line( " migrate create CreateUsersTable --open" ) + .line() + .dim( " ## Apply only the next pending migration" ) + .line( " migrate up --once" ) + .line() + .dim( " ## Roll back only the last applied migration" ) + .line( " migrate down --once" ) + .line() + .dim( " ## Preview the SQL that would run without executing it" ) + .line( " migrate up --pretend" ) + .line() + .dim( " ## Fresh database with seed data" ) + .line( " migrate fresh --seed" ) + .line() + .dim( " ## Use a named manager (multi-database support)" ) + .line( " migrate up --manager=secondary" ) + .line() + .line() + .yellowLine( "Tip: Type 'migrate --help' for detailed options on any command" ) + .line() + .dim( "Documentation: https://forgebox.io/view/commandbox-migrations" ) + .line() + .line() + } + +} diff --git a/commands/migrate/init.cfc b/commands/migrate/init.cfc index 1905df3..21fc3ad 100644 --- a/commands/migrate/init.cfc +++ b/commands/migrate/init.cfc @@ -1,16 +1,26 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- - * Initialize your project to use commandbox-migrations - * Make sure you are running this command in the root of your app. + * Initialize your project to use commandbox-migrations. * - * This will ensure the correct values are set in your box.json. + * Creates a `.cbmigrations.json` configuration file in the current working + * directory with sensible defaults. Edit this file to configure your database + * connection, migrations directory, seeders directory, and named managers for + * multi-database support. + * + * Run this command once when setting up migrations for the first time, then + * follow up with `migrate install` to create the tracking table in your database. + * + * {code:bash} + * ## Initialize migrations config in the current directory + * migrate init + * + * ## Initialize and immediately open the config file for editing + * migrate init --open + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { /** - * Initialize your project to use commandbox-migrations + * Initialize your project to use commandbox-migrations. * Make sure you are running this command in the root of your app. * * @open Open the config file after it is created. diff --git a/commands/migrate/install.cfc b/commands/migrate/install.cfc index 291f3a2..cdc8961 100644 --- a/commands/migrate/install.cfc +++ b/commands/migrate/install.cfc @@ -1,10 +1,23 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- - * Installs the migrations table in to your database. - * The migrations table keeps track of the migrations ran against your database. - * It must be installed before running any migrations. + * Install the migrations tracking table into your database. + * + * The migrations table records every migration that has been applied, allowing + * cbmigrations to know which migrations are pending and which have been run. + * This command must be run before executing `migrate up` for the first time. + * + * Running this command when the table already exists will display a message + * and exit gracefully without making any changes. + * + * {code:bash} + * ## Install the migrations table + * migrate install + * + * ## Install for a named manager + * migrate install --manager=secondary + * + * ## Install with verbose output + * migrate install --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/refresh.cfc b/commands/migrate/refresh.cfc index a470152..c8067ac 100644 --- a/commands/migrate/refresh.cfc +++ b/commands/migrate/refresh.cfc @@ -1,8 +1,23 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- * Rollback all committed migrations and then apply all migrations in order. + * + * This is the equivalent of running `migrate down` followed by `migrate up`. + * Unlike `migrate fresh`, this uses each migration's `down()` method to + * reverse changes rather than dropping all database objects directly. + * + * {code:bash} + * ## Roll back all migrations then re-apply them + * migrate refresh + * + * ## Refresh and seed the database + * migrate refresh --seed + * + * ## Refresh a named manager + * migrate refresh --manager=secondary + * + * ## Refresh with verbose error output + * migrate refresh --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/reset.cfc b/commands/migrate/reset.cfc index c9409c8..461a41b 100644 --- a/commands/migrate/reset.cfc +++ b/commands/migrate/reset.cfc @@ -1,5 +1,22 @@ /** - * Resets the database by clearing out all objects + * Reset the database by dropping all tables, views, and other schema objects. + * + * WARNING: This is a destructive operation! All schema objects will be dropped. + * No migration `down()` methods are called — the database is wiped directly. + * + * This command is used internally by `migrate fresh`. You can run it standalone + * when you want to clear the database without immediately re-running migrations. + * + * {code:bash} + * ## Drop all database objects + * migrate reset + * + * ## Reset a named manager's database + * migrate reset --manager=secondary + * + * ## Reset with verbose error output + * migrate reset --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/seed/create.cfc b/commands/migrate/seed/create.cfc index 6cd2e0a..e572034 100644 --- a/commands/migrate/seed/create.cfc +++ b/commands/migrate/seed/create.cfc @@ -2,8 +2,29 @@ * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp * www.ortussolutions.com * --- - * Create a new seeder CFC in an existing application. + * Create a new database seeder CFC in an existing application. * Make sure you are running this command in the root of your app. + * + * Seeders are used to populate your database with initial or sample data. + * Unlike migrations, seeders have no tracking — they can be run multiple times + * and each run will insert the data again. + * + * The seeder file is created in the seeds directory configured in your + * `.cbmigrations.json` file (defaults to `resources/database/seeds/`). + * + * {code:bash} + * ## Create a seeder + * migrate seed create UserSeeder + * + * ## Create a seeder and open it immediately for editing + * migrate seed create UserSeeder --open + * + * ## Create a BoxLang seeder (.bx) + * migrate seed create UserSeeder --boxlang + * + * ## Create a seeder for a named manager + * migrate seed create UserSeeder --manager=secondary + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/seed/help.cfc b/commands/migrate/seed/help.cfc new file mode 100644 index 0000000..4f32123 --- /dev/null +++ b/commands/migrate/seed/help.cfc @@ -0,0 +1,48 @@ +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Display help and usage information for the migrate seed commands. + */ +component excludeFromHelp=true extends="commandbox-migrations.models.BaseMigrationCommand" { + + function run(){ + print + .line() + .boldCyan( "Database Seeders" ) + .line() + .line() + .whiteLine( "Seeders populate your database with initial or sample data." ) + .whiteLine( "Unlike migrations, seeders can be run multiple times — each run inserts data again." ) + .line() + .boldWhiteLine( "Commands:" ) + .line() + .greenLine( " migrate seed run Run all seeders or a specific named seeder" ) + .greenLine( " migrate seed create Create a new seeder file" ) + .line() + .yellowLine( "Examples:" ) + .line() + .dim( " ## Run all seeders" ) + .line( " migrate seed run" ) + .line() + .dim( " ## Run a specific seeder by name" ) + .line( " migrate seed run UserSeeder" ) + .line() + .dim( " ## Create a new seeder and open it immediately" ) + .line( " migrate seed create UserSeeder --open" ) + .line() + .dim( " ## Create a BoxLang seeder" ) + .line( " migrate seed create UserSeeder --boxlang" ) + .line() + .dim( " ## Run seeders on a named manager" ) + .line( " migrate seed run --manager=secondary" ) + .line() + .line() + .yellowLine( "Tip: Type 'migrate seed --help' for detailed options" ) + .line() + .dim( "Documentation: https://forgebox.io/view/commandbox-migrations" ) + .line() + .line() + } + +} diff --git a/commands/migrate/seed/run.cfc b/commands/migrate/seed/run.cfc index e0dc106..1ca50ea 100644 --- a/commands/migrate/seed/run.cfc +++ b/commands/migrate/seed/run.cfc @@ -2,9 +2,28 @@ * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp * www.ortussolutions.com * --- - * Runs one or all seeders for an application against your database. - * Seeders have no concept of being ran. - * Running a seeder multiple times will insert data multiple times. + * Run one or all database seeders for an application. + * + * Seeders populate your database with initial or sample data. They are + * typically used to provide development fixtures or default application data. + * + * Unlike migrations, seeders have no tracking — they can be run as many times + * as needed and each run will insert data again. Be careful running seeders + * against a database that already contains data. + * + * {code:bash} + * ## Run all seeders + * migrate seed run + * + * ## Run a specific seeder by name + * migrate seed run UserSeeder + * + * ## Run seeders for a named manager + * migrate seed run --manager=secondary + * + * ## Run a specific seeder with verbose output + * migrate seed run UserSeeder --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/uninstall.cfc b/commands/migrate/uninstall.cfc index bdab0ea..5f46f3b 100644 --- a/commands/migrate/uninstall.cfc +++ b/commands/migrate/uninstall.cfc @@ -1,11 +1,27 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- - * Uninstalls the migrations table from your database. + * Uninstall the migrations tracking table from your database. * - * The migrations table keeps track of the migrations ran against your database. - * Uninstall it when you are removing migrations from your application. + * WARNING: Uninstalling will also run all your migrations DOWN before removing + * the tracking table. This means all applied migrations will be rolled back + * and all data managed by those migrations will be lost. + * + * Use this command when you are fully removing migrations from your application + * or want a completely clean slate. You will be asked to confirm before proceeding + * unless the --force flag is provided. + * + * {code:bash} + * ## Uninstall with confirmation prompt + * migrate uninstall + * + * ## Uninstall without confirmation + * migrate uninstall --force + * + * ## Uninstall a named manager + * migrate uninstall --manager=secondary + * + * ## Uninstall with verbose error output + * migrate uninstall --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { diff --git a/commands/migrate/up.cfc b/commands/migrate/up.cfc index f60e034..c26fd83 100644 --- a/commands/migrate/up.cfc +++ b/commands/migrate/up.cfc @@ -1,8 +1,31 @@ /** - * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp - * www.ortussolutions.com - * --- * Apply one or all pending migrations against your database. + * + * Migrations are applied in chronological order based on their timestamp prefix. + * The migrations table must be installed first via `migrate install`. + * + * {code:bash} + * ## Run all pending migrations + * migrate up + * + * ## Apply only the next pending migration + * migrate up --once + * + * ## Preview SQL without executing (dry run) + * migrate up --pretend + * + * ## Save the pretend SQL output to a file + * migrate up --pretend --file=schema.sql + * + * ## Run migrations and then seed the database + * migrate up --seed + * + * ## Run migrations for a named manager + * migrate up --manager=secondary + * + * ## Run with verbose error output + * migrate up --verbose + * {code} */ component extends="commandbox-migrations.models.BaseMigrationCommand" { From e5e2f718c8d10a9255e145ab73d9658ea85419be Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 19 Jun 2026 15:46:41 +0200 Subject: [PATCH 14/14] migrations from old approach --- commands/migrate/init.cfc | 36 ++++++++++++++++++++++++++++++--- models/BaseMigrationCommand.cfc | 17 +++++----------- templates/config.txt | 6 ++++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/commands/migrate/init.cfc b/commands/migrate/init.cfc index 21fc3ad..828629c 100644 --- a/commands/migrate/init.cfc +++ b/commands/migrate/init.cfc @@ -6,6 +6,9 @@ * connection, migrations directory, seeders directory, and named managers for * multi-database support. * + * If a legacy `.cfmigrations.json` file is detected, you will be prompted to + * rename it to `.cbmigrations.json` instead of creating a new blank config. + * * Run this command once when setting up migrations for the first time, then * follow up with `migrate install` to create the tracking table in your database. * @@ -28,16 +31,43 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { function run( boolean open = false ) { - var directory = getCWD() + var directory = getCWD() var configFileName = ".cbmigrations.json" - var configPath = "#directory#/#configFileName#" + var configPath = "#directory##configFileName#" + var legacyPath = "#directory#.cfmigrations.json" - // Check and see if the config file already exists + // Check and see if the new config file already exists if ( fileExists( configPath ) ) { print.yellowLine( "#configFileName# already exists." ) return } + // Detect legacy .cfmigrations.json and offer to migrate it + if ( fileExists( legacyPath ) ) { + print.line() + print.boldYellowLine( "A legacy '.cfmigrations.json' configuration file was detected." ) + print.yellowLine( "The config file has been renamed to '.cbmigrations.json' in this version of Migrations." ) + print.line() + + if ( confirm( "Would you like to rename '.cfmigrations.json' to '.cbmigrations.json' now? [y/n]" ) ) { + fileMove( legacyPath, configPath ) + print.greenLine( "Renamed '.cfmigrations.json' to '.cbmigrations.json' successfully." ) + print.line() + } else { + print.yellowLine( "Skipped rename. Your '.cfmigrations.json' is still in use, but consider renaming it manually." ) + print.line() + return + } + + // Open file? + if ( arguments.open ) { + openPath( configPath ) + } + + return + } + + // Create a fresh config from the template var configStub = fileRead( "/commandbox-migrations/templates/config.txt" ) file action="write" file="#configPath#" mode="777" output="#trim( configStub )#"; diff --git a/models/BaseMigrationCommand.cfc b/models/BaseMigrationCommand.cfc index 2d9756f..a47cf4d 100644 --- a/models/BaseMigrationCommand.cfc +++ b/models/BaseMigrationCommand.cfc @@ -104,25 +104,18 @@ component { */ private string function findMigrationsConfigPath( required string directory ) { // Check for the modern config file first - var cbmigrationsPath = "#arguments.directory#/.cbmigrations.json"; + var cbmigrationsPath = "#arguments.directory#/.cbmigrations.json" if ( fileExists( cbmigrationsPath ) ) { - return cbmigrationsPath; + return cbmigrationsPath } // Check for the legacy config file - var cfmigrationsPath = "#arguments.directory#/.cfmigrations.json"; + var cfmigrationsPath = "#arguments.directory#/.cfmigrations.json" if ( fileExists( cfmigrationsPath ) ) { - print.boldYellowLine( "The config file .cfmigrations.json has been renamed to .cbmigrations.json in this new version of Migrations" ); - if ( confirm( "Would you like me to rename it for you? [y/n]" ) ) { - fileMove( cfmigrationsPath, cbmigrationsPath ); - print.greenLine( "Renamed .cfmigrations.json to .cbmigrations.json." ); - return cbmigrationsPath; - } - print.line( "Continuing with .cfmigrations.json, but consider renaming it manually." ); - return cfmigrationsPath; + return cfmigrationsPath } - return ""; + return "" } /** diff --git a/templates/config.txt b/templates/config.txt index 76dea33..103e92f 100644 --- a/templates/config.txt +++ b/templates/config.txt @@ -11,9 +11,11 @@ "type": "${DB_DRIVER}", "database": "${DB_DATABASE}", "host": "${DB_HOST}", - "port": ${DB_PORT}, + "port": "${DB_PORT}", "username": "${DB_USER}", - "password": "${DB_PASSWORD}" + "password": "${DB_PASSWORD}", + "bundleName": "${DB_BUNDLENAME}", + "bundleVersion": "${DB_BUNDLEVERSION}" } } }