Skip to content

Commit 37cbb1d

Browse files
authored
feat: Add Seeders and multiple Migration Managers
Seeders are now available to quickly set up a development environment with data. Multiple migration managers can be configured to allow migrating multiple databases, ElasticSearch instances, and more! See the README for more information. BREAKING CHANGE: Multiple migration managers have changed the configuration structure in `config/ColdBox.cfc`. If you modified your configuration at all, you will need to update to the new structure. See README for more details. Additionally, removed ACF 11 and ACF 2016 support. (ACF 2016 should still work, but it is not tested.)
1 parent 68374f1 commit 37cbb1d

37 files changed

+1436
-1639
lines changed

.cfformat.json

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,58 @@
11
{
2-
"array.empty_padding": false,
3-
"array.padding": true,
4-
"array.multiline.min_length": 40,
5-
"array.multiline.element_count": 2,
6-
"array.multiline.leading_comma.padding": true,
7-
"array.multiline.leading_comma": false,
8-
"alignment.consecutive.assignments": true,
9-
"alignment.consecutive.properties": true,
10-
"alignment.consecutive.params": true,
11-
"brackets.padding": true,
12-
"comment.asterisks": "align",
13-
"binary_operators.padding": true,
14-
"for_loop_semicolons.padding": true,
15-
"function_call.empty_padding": false,
16-
"function_call.padding": true,
17-
"function_call.multiline.leading_comma.padding": true,
18-
"function_call.casing.builtin": "cfdocs",
19-
"function_call.casing.userdefined": "camel",
20-
"function_call.multiline.element_count": 3,
21-
"function_call.multiline.leading_comma": false,
22-
"function_call.multiline.min_length": 40,
23-
"function_declaration.padding": true,
24-
"function_declaration.empty_padding": false,
25-
"function_declaration.multiline.leading_comma": false,
26-
"function_declaration.multiline.leading_comma.padding": true,
27-
"function_declaration.multiline.element_count": 3,
28-
"function_declaration.multiline.min_length": 40,
29-
"function_declaration.group_to_block_spacing": "spaced",
30-
"function_anonymous.empty_padding": false,
31-
"function_anonymous.group_to_block_spacing": "spaced",
32-
"function_anonymous.multiline.element_count": 3,
33-
"function_anonymous.multiline.leading_comma": false,
34-
"function_anonymous.multiline.leading_comma.padding": true,
35-
"function_anonymous.multiline.min_length": 40,
36-
"function_anonymous.padding": true,
37-
"indent_size": 4,
38-
"keywords.block_to_keyword_spacing": "spaced",
39-
"keywords.group_to_block_spacing": "spaced",
40-
"keywords.padding_inside_group": true,
41-
"keywords.spacing_to_block": "spaced",
42-
"keywords.spacing_to_group": true,
43-
"keywords.empty_group_spacing": false,
44-
"max_columns": 120,
45-
"metadata.multiline.element_count": 3,
46-
"metadata.multiline.min_length": 40,
47-
"newline":"\n",
48-
"property.multiline.element_count": 3,
49-
"property.multiline.min_length": 40,
50-
"parentheses.padding": true,
51-
"strings.quote": "double",
52-
"strings.convertNestedQuotes": false,
53-
"strings.attributes.quote": "double",
54-
"struct.separator": " : ",
55-
"struct.padding": true,
56-
"struct.empty_padding": false,
57-
"struct.multiline.leading_comma": false,
58-
"struct.multiline.leading_comma.padding": true,
59-
"struct.multiline.element_count": 2,
60-
"struct.multiline.min_length": 40,
61-
"tab_indent": true
2+
"alignment.consecutive.assignments":false,
3+
"alignment.consecutive.params":false,
4+
"alignment.consecutive.properties":false,
5+
"array.empty_padding":false,
6+
"array.multiline.element_count":4,
7+
"array.multiline.leading_comma":false,
8+
"array.multiline.leading_comma.padding":true,
9+
"array.multiline.min_length":40,
10+
"array.padding":true,
11+
"binary_operators.padding":true,
12+
"brackets.padding":true,
13+
"comment.asterisks":"align",
14+
"for_loop_semicolons.padding":true,
15+
"function_anonymous.empty_padding":false,
16+
"function_anonymous.group_to_block_spacing":"spaced",
17+
"function_anonymous.multiline.element_count":4,
18+
"function_anonymous.multiline.leading_comma":false,
19+
"function_anonymous.multiline.leading_comma.padding":true,
20+
"function_anonymous.multiline.min_length":40,
21+
"function_anonymous.padding":true,
22+
"function_call.casing.builtin":"cfdocs",
23+
"function_call.casing.userdefined":"camel",
24+
"function_call.empty_padding":false,
25+
"function_call.multiline.element_count":4,
26+
"function_call.multiline.leading_comma":false,
27+
"function_call.multiline.leading_comma.padding":true,
28+
"function_call.multiline.min_length":40,
29+
"function_call.padding":true,
30+
"function_declaration.empty_padding":false,
31+
"function_declaration.group_to_block_spacing":"spaced",
32+
"function_declaration.multiline.element_count":4,
33+
"function_declaration.multiline.leading_comma":false,
34+
"function_declaration.multiline.leading_comma.padding":true,
35+
"function_declaration.multiline.min_length":40,
36+
"function_declaration.padding":true,
37+
"indent_size":4,
38+
"keywords.block_to_keyword_spacing":"spaced",
39+
"keywords.empty_group_spacing":false,
40+
"keywords.group_to_block_spacing":"spaced",
41+
"keywords.padding_inside_group":true,
42+
"keywords.spacing_to_block":"spaced",
43+
"keywords.spacing_to_group":true,
44+
"max_columns":120,
45+
"method_call.chain.multiline":3,
46+
"newline":"\n",
47+
"parentheses.padding":true,
48+
"strings.attributes.quote":"double",
49+
"strings.quote":"double",
50+
"struct.empty_padding":false,
51+
"struct.multiline.element_count":4,
52+
"struct.multiline.leading_comma":false,
53+
"struct.multiline.leading_comma.padding":true,
54+
"struct.multiline.min_length":40,
55+
"struct.padding":true,
56+
"struct.separator":": ",
57+
"tab_indent":false
6258
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ orgluceepostgresql830jdbc4lex/
1414

1515
.env
1616
.tmp
17+
.vscode/**

ModuleConfig.cfc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ component {
55
this.description = "Keep track and run your database migrations with CFML";
66
this.version = "0.0.0";
77
this.cfmapping = "cfmigrations";
8-
this.autoMapModels = false;
98
this.dependencies = [ "qb" ];
109

1110
function configure() {
1211
settings = {
13-
migrationsDirectory = "/resources/database/migrations",
14-
defaultGrammar = "BaseGrammar"
12+
"managers": {
13+
"default": {
14+
"manager": "cfmigrations.models.QBMigrationManager",
15+
"properties": { "defaultGrammar": "BaseGrammar" }
16+
}
17+
}
1518
};
1619

17-
binder.map( "MigrationService@cfmigrations" )
18-
.to( "#moduleMapping#.models.MigrationService" )
19-
.initArg( name = "defaultGrammar", ref = "#settings.defaultGrammar#@qb" );
20+
binder.getInjector().registerDSL( "migrationService", "#moduleMapping#.dsl.MigrationServiceDSL" );
2021
}
2122

2223
}

README.md

Lines changed: 115 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Overview
66

7-
Database migrations are a way of providing version control for your application's database. Changes to database schema are kept in timestamped files that are ran in order `up` and `down`.In the `up` function, you describe the changes to apply your migration. In the `down` function, you describe the changes to undo your migration.
7+
Migrations are a way of providing version control for your application's schema. Changes to schema are kept in timestamped files that are ran in order `up` and `down`.In the `up` function, you describe the changes to apply your migration. In the `down` function, you describe the changes to undo your migration.
88

99
Here's a simple example of that using simple `queryExecute`:
1010

@@ -41,34 +41,99 @@ An easy way to generate these files is to use `commandbox-migrations` and the `m
4141

4242
### Installation and Uninstallation
4343

44-
In order to track which migrations have been ran, `cfmigrations` needs to install a table in your database called `cfmigrations`. You can do this by calling the `install()` method or by running the `migrate install` command from `commandbox-migrations`.
44+
In order to track which migrations have been ran, `cfmigrations` needs to install a tracking mechanism. For database migrations this creates a table in your database called `cfmigrations` by default. You can do this by calling the `install()` method or by running the `migrate install` command from `commandbox-migrations`.
4545

46-
If you find a need to, you can uninstall the migrations table by calling the `uninstall()` method or by running `migrate uninstall` from `commandbox-migrations`. Running this method will rollback all ran migrations before dropping the `cfmigrations` table.
46+
If you find a need to, you can uninstall the migrations tracker by calling the `uninstall()` method or by running `migrate uninstall` from `commandbox-migrations`. Running this method will rollback all ran migrations before removing the migrations tracker.
4747

48-
### Setting Schema
48+
### Configuration
4949

50-
It's important to set the `schema` attribute for `cfmigrations`. Without it, `cfmigrations` can't tell the difference
51-
between a migration table installed in the schema you want and any other schema on the same database. You can
52-
set the schema by calling the `setSchema( string schema )` method.
50+
The module is configured by default with a single migration service that interact with your database, optionally using qb. Multiple migration services with different managers may also be configured. The default manager for the cfmigrations is `QBMigrationManager`, but you may use others, such as those included with the `cbmongodb` and `cbelasticsearch` modules or roll your own.
51+
52+
The default configuration for the module settings are:
53+
54+
```cfc
55+
moduleSettings = {
56+
"cfmigrations" : {
57+
"managers" : {
58+
"default" : {
59+
// The manager handling and executing the migration files
60+
"manager" : "cfmigrations.models.QBMigrationManager",
61+
// The directory containing the migration files
62+
"migrationsDirectory" : "/resources/database/migrations",
63+
// The directory containing any seeds, if applicable
64+
"seedsDirectory" : "/resources/database/seeds",
65+
// A comma-delimited list of environments which are allowed to run seeds
66+
"seedEnvironments" : "development"
67+
"properties" : {
68+
"defaultGrammar" : "BaseGrammar@qb"
69+
}
70+
}
71+
}
72+
}
73+
}
74+
```
75+
76+
With this configuration, the `default` migration manager may be retrieved via WireBox at `migrationService:default`.
77+
78+
Here is an example of a multi-manager migrations system. Each separate manager will require their own configuration properties.
79+
80+
```cfc
81+
moduleSettings = {
82+
"cfmigrations": {
83+
"managers": {
84+
"db1": {
85+
"manager": "cfmigrations.models.QBMigrationManager",
86+
"migrationsDirectory": "/resources/database/db1/migrations",
87+
"seedsDirectory": "/resources/database/db1/seeds",
88+
"properties": {
89+
"defaultGrammar": "MySQLGrammar@qb",
90+
"datasource": "db1",
91+
"useTransactions": "false",
92+
}
93+
},
94+
"db2": {
95+
"manager": "cfmigrations.models.QBMigrationManager",
96+
"migrationsDirectory": "/resources/database/db2/migrations",
97+
"seedsDirectory": "/resources/database/db2/seeds",
98+
"properties": {
99+
"defaultGrammar": "PostgresGrammar@qb",
100+
"datasource": "db2",
101+
"useTransactions": "true"
102+
}
103+
},
104+
"elasticsearch": {
105+
"manager": "cbelasticearch.models.migrations.Manager",
106+
"migrationsDirectory": "/resources/elasticsearch/migrations"
107+
}
108+
}
109+
}
110+
};
111+
```
112+
113+
With this configuration the individual migration managers would be retreived as such:
114+
115+
- `db1` - getInstance( "migrationService:db1" )
116+
- `db2` - getInstance( "migrationService:db2" )
117+
- `elasticsearch` - getInstance( "migrationService:elasticsearch" )
53118

54119
### Migration Files
55120

56-
A migration file is a component with two methods `up` and `down`. The function `up` should define how to apply the migration. The function `down` should define how to undo the change down in `up`. The `up` and `down` functions are passed an instance of `SchemaBuilder@qb` and `QueryBuilder@qb` as arguments. To learn more about the functionality and benefits of `SchemaBuilder`, `QueryBuilder`, and `qb`, please [read the QB documentation here](https://qb.ortusbooks.com/). In brief, `qb` offers a fluent, expressive syntax that can be compiled to many different database grammars, providing both readability and flexibility.
121+
A migration file is a component with two methods `up` and `down`. The function `up` should define how to apply the migration. The function `down` should define how to undo the change down in `up`. For `QBMigrationManager` migrations (which is the default), the `up` and `down` functions are passed an instance of `SchemaBuilder@qb` and `QueryBuilder@qb` as arguments. To learn more about the functionality and benefits of `SchemaBuilder`, `QueryBuilder`, and `qb`, please [read the QB documentation here](https://qb.ortusbooks.com/). In brief, `qb` offers a fluent, expressive syntax that can be compiled to many different database grammars, providing both readability and flexibility.
57122

58123
Here's the same example as above using qb's `SchemaBuilder`:
59124

60125
```cfc
61126
component {
62127
63-
function up( SchemaBuilder schema, QueryBuilder query ) {
64-
schema.create( "users", function( Blueprint table ) {
65-
table.increments( "id" );
66-
table.string( "email" );
67-
table.string( "password" );
68-
} );
128+
function up( schema, qb ) {
129+
schema.create( "users", function( t ) {
130+
t.increments( "id" );
131+
t.string( "email" );
132+
t.string( "password" );
133+
} );
69134
}
70135
71-
function down( SchemaBuilder schema, QueryBuilder query ) {
136+
function down( schema, qb ) {
72137
schema.drop( "users" );
73138
}
74139
@@ -77,7 +142,7 @@ component {
77142

78143
Migration files need to follow a specific naming convention — `YYYY_MM_DD_HHMISS_[describe_your_changes_here].cfc`. This is how `cfmigrations` knows in what order to run your migrations. Generating these files is made easier with the `migrate create` command from `commandbox-migrations`.
79144

80-
In addition to schema changes, you can seed your database with data. This is especially useful when adding new columns and needing to seed the new columns with the correct data.
145+
Using the injected `qb` instance, you can insert or update required data for your application. If you want to create test data for your application, take a look at seeders below instead.
81146

82147
There is no limit to what you can do in a migration. It is recommended that you separate changes to different tables to separate migration files to keep things readable.
83148

@@ -137,8 +202,42 @@ Returns `true` if there are available migrations which can be run in the provide
137202
| --------- | -------- | -------- | --------------- | ------------------------------------------------------------------------- |
138203
| direction | String | `true` | | The direction for which to run the available migrations — `up` or `down`. |
139204

205+
### Seeders
206+
207+
Seeding your database is an optional step that allows you to add data to your database in mass. It is usually used in development to create a populated environment for testing. Seeders should not be used for data required to run your application or to migrate data between columns. Seeders should be seen as entirely optional. If a seeder is never ran, your application should still work.
208+
209+
Seeders can be ran by calling the `seed` method on a `MigrationService`. It takes an optional `seedName` string to only run a specific seeder. Additionally, you can run all your seeders when migrating your database by passing `seed = true` to the `up` method.
210+
211+
By default, seeders can only be ran in `development` environments. This can be configured on each `manager` by setting a `seedEnvironments` key to either a list or array of allowed environments to run in.
212+
213+
A seeder is a cfc file with a single required method - `run`. For the `QBMigrationManager`, it is passed a `QueryBuilder` instance and a `MockData` instance, useful for creating fake data to insert into your database. (Other Migration Managers will have other arguments passed. Please refer to the documentation for your specific manager.)
214+
215+
```cfc
216+
component {
217+
218+
function run( qb, mockdata ) {
219+
qb.table( "users" ).insert(
220+
mockdata.mock(
221+
$num = 25,
222+
"firstName": "firstName",
223+
"lastName": "lastName",
224+
"email": "email",
225+
"password": "string-secure"
226+
)
227+
);
228+
}
229+
230+
}
231+
```
232+
140233
### Tips and tricks
141234

235+
#### Setting Schema
236+
237+
It's important to set the `schema` attribute for `cfmigrations`. Without it, `cfmigrations` can't tell the difference
238+
between a migration table installed in the schema you want and any other schema on the same database. You can
239+
set the schema by calling the `setSchema( string schema )` method.
240+
142241
#### Default values in MS SQL server
143242

144243
MS SQL server requires some special treatment when removing columns with default values. Even though syntax is almost the same, MS SQL creates a special default constraint like `DF_tablename_columname`. When migrating down, this constraint has to be removed before dropping the column. In other grammars no special named constraint is created.

box.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
"schema"
2222
],
2323
"scripts":{
24-
"format":"cfformat run models/**/*.cfc,tests/specs/**/*.cfc --overwrite",
25-
"format:check":"cfformat check models/**/*.cfc,tests/specs/**/*.cfc --verbose"
24+
"format":"cfformat run models/**/*.cfc,dsl/**/*.cfc,tests/specs/**/*.cfc,ModuleConfig.cfc --overwrite",
25+
"format:check":"cfformat check models/**/*.cfc,dsl/**/*.cfc,tests/specs/**/*.cfc,ModuleConfig.cfc --verbose"
2626
},
2727
"private":false,
2828
"license":[
@@ -32,16 +32,18 @@
3232
}
3333
],
3434
"dependencies":{
35-
"qb":"^8.0.0"
35+
"qb":"^8.0.0",
36+
"mockdatacfc":"^3.4.0+35"
3637
},
3738
"installPaths":{
3839
"qb":"modules/qb/",
3940
"testbox":"testbox/",
40-
"coldbox":"tests/resources/app/coldbox/"
41+
"coldbox":"tests/resources/app/coldbox/",
42+
"mockdatacfc":"modules/mockdatacfc/"
4143
},
4244
"devDependencies":{
43-
"testbox":"^3.2.0+356",
44-
"coldbox":"^5.6.2+1148",
45+
"testbox":"^4",
46+
"coldbox":"^6",
4547
"orgpostgresqljdbc424214lex":"lex:https://ext.lucee.org/org.postgresql.jdbc42-42.1.4.lex"
4648
},
4749
"ignore":[

dsl/MigrationServiceDSL.cfc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Processes WireBox DSL's starting with "migrationService:"
3+
*/
4+
component {
5+
6+
/**
7+
* Creates the Migration Service DSL Processor.
8+
*
9+
* @injector The WireBox injector.
10+
*
11+
* @return MigrationServiceDSL
12+
*/
13+
public MigrationServiceDSL function init( required Injector injector ) {
14+
variables.injector = arguments.injector;
15+
return this;
16+
}
17+
18+
/**
19+
* Creates a MigrationService from the dsl.
20+
* The portion after the colon is used to identifier the manager.
21+
*
22+
* @definition The dsl struct definition.
23+
*
24+
* @return MigrationService
25+
*/
26+
public MigrationService function process( required struct definition ) {
27+
var settings = variables.injector.getInstance( dsl = "coldbox:moduleSettings:cfmigrations" );
28+
return variables.injector.getInstance(
29+
name = "MigrationService@cfmigrations",
30+
initArguments = settings.managers[ listRest( arguments.definition.dsl, ":" ) ]
31+
);
32+
}
33+
34+
}

0 commit comments

Comments
 (0)