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/.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..76afdb6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Tests + +on: + push: + branches: + - master + - development + 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 ea99fc0..ad94da0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ /modules/ +/testbox/ +.test-tmp/ 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/.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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..99d7297 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,177 @@ +# 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` | + +## 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 | diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 1bc51fb..3667f83 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -1,7 +1,16 @@ -component { +/** + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + */ + 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/README.md b/README.md index a711ad7..81257db 100644 --- a/README.md +++ b/README.md @@ -1,17 +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`. -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 { @@ -22,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}", @@ -46,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}", @@ -63,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}", @@ -84,7 +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`: +You need to create a `.cbmigrations.json` config file in your application root folder. You can do this easily by running `migrate init`. ```json { @@ -95,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}", @@ -128,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 @@ -143,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( " @@ -183,10 +224,9 @@ DB_USER=test DB_PASSWORD=pass1234 ``` - I recommend adding this file to your `.gitignore` -``` +```bash .env ``` @@ -202,32 +242,66 @@ DB_PASSWORD= You would update your `.gitignore` file to not ignore the `.env.example` file: -``` +```bash .env !.env.example ``` +## BoxLang Support + +`commandbox-migrations` fully supports [BoxLang](https://boxlang.io) projects: + +- 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: + +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` +Every command below accepts an optional `manager` argument (defaulting to `default`) +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. -Creates the migration config file as `.cfmigrations.json`, if it doesn't already exist. +### `migrate init [--open] [--boxlang] [--no-boxlang]` -### `migrate install` +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`. -Installs the migration table in to your database. +Passing `--open` opens the config file once it's created. + +### `migrate install [manager] [--verbose]` + +Installs the migration table for the given manager in to your database. This migration table keeps track of the ran migrations. -### `migrate create [name]` +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. +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. @@ -241,10 +315,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. @@ -258,28 +332,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 `cbmigrations` 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]` +Removes the `cbmigrations` table for the given manager after running down +any ran migrations. Prompts for confirmation before uninstalling unless +`--force` is passed. + +Passing the `--verbose` flag will show the resolved migrations config +as well as the full stack trace of any errors. -Creates a new Seeder file. +### `migrate seed create [name] [manager] [--open] [--boxlang] [--no-boxlang]` -### `migrate seed run` +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`). -Runs one or all Seeders. +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. diff --git a/box.json b/box.json index f60e33a..0feccab 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,19 +11,20 @@ }, "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", "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/commands/migrate/create.cfc b/commands/migrate/create.cfc index 7f62810..baa30af 100644 --- a/commands/migrate/create.cfc +++ b/commands/migrate/create.cfc @@ -4,40 +4,59 @@ * * 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" { /** - * @name.hint Name of the migration to create without the .cfc. - * @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. + * @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 ) { - setup( manager = arguments.manager, setupDatasource = false ); - - var migrationsDirectory = expandPath( variables.migrationService.getMigrationsDirectory() ); + function run( + required string name, + string manager = "default", + boolean open = false, + boolean boxlang = isBoxLangProject( getCWD() ) + ) { + setup( manager = arguments.manager, setupDatasource = false ) + var migrationsDirectory = expandPath( variables.migrationService.getMigrationsDirectory() ) // Validate migrationsDirectory if ( !directoryExists( migrationsDirectory ) ) { - directoryCreate( migrationsDirectory ); + directoryCreate( migrationsDirectory ) } - var timestamp = dateTimeFormat( now(), "yyyy_mm_dd_HHnnss" ); - var migrationPath = "#migrationsDirectory##timestamp#_#arguments.name#.cfc"; - - 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/down.cfc b/commands/migrate/down.cfc index de14e90..19ed35b 100644 --- a/commands/migrate/down.cfc +++ b/commands/migrate/down.cfc @@ -1,15 +1,38 @@ /** * 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" { /** - * @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, @@ -21,8 +44,8 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/commands/migrate/fresh.cfc b/commands/migrate/fresh.cfc index 24fa27b..f6b78a6 100644 --- a/commands/migrate/fresh.cfc +++ b/commands/migrate/fresh.cfc @@ -1,20 +1,40 @@ /** - * 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" { /** - * @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 ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); 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 4eb8ea1..828629c 100644 --- a/commands/migrate/init.cfc +++ b/commands/migrate/init.cfc @@ -1,34 +1,82 @@ /** - * 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. + * + * 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. + * + * {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 { +component extends="commandbox-migrations.models.BaseMigrationCommand" { - property name="packageService" inject="PackageService"; - property name="JSONService" inject="JSONService"; + /** + * 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. + */ + function run( + boolean open = false + ) { + var directory = getCWD() + var configFileName = ".cbmigrations.json" + var configPath = "#directory##configFileName#" + var legacyPath = "#directory#.cfmigrations.json" - function run( boolean open = false ) { - var directory = getCWD(); + // Check and see if the new config file already exists + if ( fileExists( configPath ) ) { + print.yellowLine( "#configFileName# already exists." ) + return + } - var configPath = "#directory#/.cfmigrations.json"; + // 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() - // Check and see if a .cfmigrations.json file exists - if ( fileExists( configPath ) ) { - print.yellowLine( ".cfmigrations.json already exists." ); - return; + 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 } - var configStub = fileRead( "/commandbox-migrations/templates/config.txt" ); + // 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 )#"; - print.greenLine( "Created .cfmigrations config file." ); + print.greenLine( "Created #configFileName# config file." ) // Open file? if ( arguments.open ) { - openPath( configPath ); + openPath( configPath ) } } diff --git a/commands/migrate/install.cfc b/commands/migrate/install.cfc index a30d11c..cdc8961 100644 --- a/commands/migrate/install.cfc +++ b/commands/migrate/install.cfc @@ -1,22 +1,37 @@ /** - * Installs the cfmigrations table in to your database. + * Install the migrations tracking table into your database. * - * The cfmigrations table keeps track of the migrations ran against your database. - * It must be installed before running any migrations. + * 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" { /** - * @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 ); if ( verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } try { diff --git a/commands/migrate/refresh.cfc b/commands/migrate/refresh.cfc index b428364..c8067ac 100644 --- a/commands/migrate/refresh.cfc +++ b/commands/migrate/refresh.cfc @@ -1,20 +1,38 @@ /** * 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" { /** - * @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 ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/commands/migrate/reset.cfc b/commands/migrate/reset.cfc index 9430ef4..461a41b 100644 --- a/commands/migrate/reset.cfc +++ b/commands/migrate/reset.cfc @@ -1,19 +1,36 @@ /** - * 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" { /** - * @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 ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } try { diff --git a/commands/migrate/seed/create.cfc b/commands/migrate/seed/create.cfc index 5ada685..e572034 100644 --- a/commands/migrate/seed/create.cfc +++ b/commands/migrate/seed/create.cfc @@ -1,36 +1,66 @@ /** - * Create a new seeder CFC in an existing application. + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * 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" { /** - * @name.hint Name of the seeder to create without the .cfc. - * @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. + * @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 ) { - 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 seedPath = "#seedsDirectory##arguments.name#.cfc"; - - 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/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 8dd60b5..1ca50ea 100644 --- a/commands/migrate/seed/run.cfc +++ b/commands/migrate/seed/run.cfc @@ -1,27 +1,49 @@ /** - * 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. + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * 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" { /** - * @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 ); - 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.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); @@ -39,7 +61,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() ); @@ -68,10 +90,14 @@ 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 ); - 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..5f46f3b 100644 --- a/commands/migrate/uninstall.cfc +++ b/commands/migrate/uninstall.cfc @@ -1,23 +1,42 @@ /** - * Uninstalls the cfmigrations table from your database. + * Uninstall the migrations tracking 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. + * 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" { /** - * @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 cbmigrations. */ function run( string manager = "default", boolean verbose = false, boolean force = false ) { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); @@ -30,7 +49,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 4105f57..c26fd83 100644 --- a/commands/migrate/up.cfc +++ b/commands/migrate/up.cfc @@ -1,16 +1,42 @@ /** * 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" { /** - * @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", @@ -23,8 +49,8 @@ component extends="commandbox-migrations.models.BaseMigrationCommand" { setup( arguments.manager ); if ( arguments.verbose ) { - print.blackOnYellowLine( "cfmigrations info:" ); - print.line( getCFMigrationsInfo() ).line(); + print.blackOnYellowLine( "cbmigrations info:" ); + print.line( getMigrationsInfo() ).line(); } pagePoolClear(); diff --git a/models/BaseMigrationCommand.cfc b/models/BaseMigrationCommand.cfc index 1c52892..a47cf4d 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"; @@ -6,9 +11,14 @@ component { property name="sqlHighlighter" inject="sqlHighlighter"; property name="systemSettings" inject="SystemSettings"; 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 = getCFMigrationsInfo(); + var config = getMigrationsInfo(); if ( !config.keyExists( arguments.manager ) ) { error( "No manager found named [#arguments.manager#]. Available managers are: #config.keyList( ", " )#" ); } @@ -42,14 +52,22 @@ component { ); } - function installDatasource( required struct connectionInfo, string datasourceName = "cfmigrations" ) { + /** + * 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 = "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; } + /** + * 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 ) @@ -57,10 +75,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]" ) ) { @@ -71,17 +96,70 @@ component { } } - private struct function getCFMigrationsInfo() { - var cfmigrationsInfoType = "boxJSON"; + /** + * Returns the path to the first migrations config file found in the given directory, + * 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 ) { + // 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 ) ) { + return cfmigrationsPath + } + + 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. + * + * @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 + } + + // 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 + } + /** + * 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 .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 .cbmigrations.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 +175,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 +197,7 @@ component { properties[ "schema" ] = boxJSONMigrationsInfo.schema; } - var cfmigrationsInfo = { + var migrationsInfo = { "default": { "manager": "cfmigrations.models.QBMigrationManager", "migrationsDirectory": boxJSONMigrationsInfo.migrationsDirectory, @@ -127,16 +205,20 @@ component { } }; - variables.systemSettings.expandDeepSystemSettings( cfmigrationsInfo ); - return cfmigrationsInfo; + variables.systemSettings.expandDeepSystemSettings( migrationsInfo ); + return migrationsInfo; } - private string function getCFMigrationsType() { + /** + * 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 .cfmigrations.json file exists - if ( fileExists( "#directory#/.cfmigrations.json" ) ) { - return "cfmigrations"; + // Check and see if a .cbmigrations.json or .cbmigrations.json file exists + if ( len( findMigrationsConfigPath( directory ) ) ) { + return "cbmigrations"; } // Check and see if box.json exists @@ -148,23 +230,30 @@ component { var boxJSONMigrationsInfo = JSONService.show( boxJSON, "cfmigrations", {} ); if ( boxJSONMigrationsInfo.keyExists( "managers" ) ) { - return "cfmigrations"; + return "cbmigrations"; } 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 = 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" } ) ); } - private string function startsWith( required string word, required string substring ) { + /** + * Returns true if `word` starts with `substring` (or `substring` is empty). + */ + private boolean function startsWith( required string word, required string substring ) { if ( len( arguments.substring ) == 0 ) { return true; } 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" + } + } +} 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..103e92f 100644 --- a/templates/config.txt +++ b/templates/config.txt @@ -6,14 +6,16 @@ "properties": { "defaultGrammar": "AutoDiscover@qb", "schema": "${DB_SCHEMA}", - "migrationsTable": "cfmigrations", + "migrationsTable": "cbmigrations", "connectionInfo": { "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}" } } } 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 ) { + + } + +} diff --git a/tests/Runner.cfc b/tests/Runner.cfc new file mode 100644 index 0000000..0e4e160 --- /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 = "text" + ) + + 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" ); + } ); + + } ); + } + +}