@verdnatura/myt 1.5.22 → 1.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -8
- package/assets/structure.sql +5 -0
- package/myt-clean.js +10 -7
- package/myt-push.js +49 -69
- package/myt-run.js +1 -1
- package/myt-version.js +3 -3
- package/myt.js +64 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Each command can have its own specific commandline options.
|
|
|
64
64
|
|
|
65
65
|
## Basic information
|
|
66
66
|
|
|
67
|
-
First of all you have to
|
|
67
|
+
First of all you have to initialize the workspace.
|
|
68
68
|
|
|
69
69
|
```text
|
|
70
70
|
$ myt init
|
|
@@ -72,7 +72,7 @@ $ myt init
|
|
|
72
72
|
|
|
73
73
|
Now you can configure Myt using *myt.config.yml* file, located at the root of
|
|
74
74
|
your workspace. This file should include the project codename and schemas/tables
|
|
75
|
-
|
|
75
|
+
which are exported when you use *pull* or *dump* commands.
|
|
76
76
|
|
|
77
77
|
Don't forget to initialize git (if it isn't initialized yet).
|
|
78
78
|
|
|
@@ -143,6 +143,38 @@ Don't place your PL/SQL objects here, use the routines folder!
|
|
|
143
143
|
`- 00-sameNumbers.sql
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
### Environment-Specific Versioning with Realms
|
|
147
|
+
|
|
148
|
+
#### Overview
|
|
149
|
+
|
|
150
|
+
We have introduced a new feature that allows users to apply version-specific changes based on their configured environment, or "realm". This ensures that certain changes are only applied when the user is operating within a specific realm, providing an additional layer of customization and control.
|
|
151
|
+
|
|
152
|
+
#### Configuration
|
|
153
|
+
|
|
154
|
+
To make use of this feature, you need to configure your realm in the `versionConfig` table. Set your realm by inserting or updating a record in this table. The `realm` field should contain the identifier of your environment.
|
|
155
|
+
|
|
156
|
+
#### File Naming Convention
|
|
157
|
+
|
|
158
|
+
To designate a file as realm-specific, include a `.your_realm.` segment before the file extension. For example, if your realm is set to 'ab', the file should be named like this: `filename.ab.sql`.
|
|
159
|
+
|
|
160
|
+
#### How It Works
|
|
161
|
+
|
|
162
|
+
1. **Set your Realm**: Configure your realm in the `versionConfig` table.
|
|
163
|
+
|
|
164
|
+
2. **Add Files**: Place your realm-specific files in the `versions` folder. Make sure to follow the naming convention.
|
|
165
|
+
|
|
166
|
+
3. **Version Order**: Files are applied in the existing version order. In addition to that, the realm is validated.
|
|
167
|
+
|
|
168
|
+
4. **Apply Changes**: Run the usual versioning commands. The realm-specific files will only be applied if your configured realm matches the realm in the file name.
|
|
169
|
+
|
|
170
|
+
#### Important Notes
|
|
171
|
+
|
|
172
|
+
- If no realm is configured, realm-specific files will be ignored.
|
|
173
|
+
|
|
174
|
+
- If you have a realm configured but the realm-specific files belong to a different realm, those files will also be ignored.
|
|
175
|
+
|
|
176
|
+
This feature allows for greater flexibility when working in different environments, making it easier to manage realm-specific changes in a shared repository.
|
|
177
|
+
|
|
146
178
|
### Local server
|
|
147
179
|
|
|
148
180
|
The local server is created as a MariaDB Docker container using the base dump
|
|
@@ -169,6 +201,7 @@ Initializes an empty workspace.
|
|
|
169
201
|
$ myt init
|
|
170
202
|
```
|
|
171
203
|
|
|
204
|
+
|
|
172
205
|
### pull
|
|
173
206
|
|
|
174
207
|
Incorporates database routine changes into workspace.
|
|
@@ -263,14 +296,14 @@ $ myt start
|
|
|
263
296
|
|
|
264
297
|
## Why
|
|
265
298
|
|
|
266
|
-
The main reason for starting this project
|
|
299
|
+
The main reason for starting this project is because there are no fully free
|
|
267
300
|
and open source migration tools available that allow versioning database
|
|
268
|
-
routines with
|
|
301
|
+
routines with a standard CVS system as if they were normal application code.
|
|
269
302
|
|
|
270
303
|
Also, the existing tools are too complex and require too much knowledge to
|
|
271
|
-
|
|
304
|
+
initiate a small project.
|
|
272
305
|
|
|
273
|
-
##
|
|
306
|
+
## To-Do
|
|
274
307
|
|
|
275
308
|
* Undo changes when there is an error applying a version using "undo" files.
|
|
276
309
|
* Console logging via events.
|
|
@@ -280,6 +313,6 @@ start a small project.
|
|
|
280
313
|
## Built With
|
|
281
314
|
|
|
282
315
|
* [Git](https://git-scm.com/)
|
|
283
|
-
* [
|
|
316
|
+
* [Node.js](https://nodejs.org/)
|
|
284
317
|
* [NodeGit](https://www.nodegit.org/)
|
|
285
|
-
* [
|
|
318
|
+
* [Docker](https://www.docker.com/)
|
package/assets/structure.sql
CHANGED
|
@@ -22,3 +22,8 @@ CREATE TABLE `versionLog` (
|
|
|
22
22
|
|
|
23
23
|
ALTER TABLE `versionLog`
|
|
24
24
|
ADD PRIMARY KEY (`code`,`number`,`file`);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE `util`.`versionConfig` (
|
|
27
|
+
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
28
|
+
`realm` VARCHAR(16) NULL DEFAULT NULL COMMENT 'Data set on which the project runs'
|
|
29
|
+
) ENGINE=InnoDB;
|
package/myt-clean.js
CHANGED
|
@@ -19,16 +19,19 @@ class Clean extends Command {
|
|
|
19
19
|
|
|
20
20
|
async run(myt, opts) {
|
|
21
21
|
await myt.dbConnect();
|
|
22
|
-
const
|
|
23
|
-
const number =
|
|
22
|
+
const dbVersion = await myt.fetchDbVersion() || {};
|
|
23
|
+
const number = parseInt(dbVersion.number);
|
|
24
24
|
|
|
25
25
|
const oldVersions = [];
|
|
26
26
|
const versionDirs = await fs.readdir(opts.versionsDir);
|
|
27
27
|
for (const versionDir of versionDirs) {
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
const version = await myt.loadVersion(versionDir);
|
|
29
|
+
const shouldArchive = version
|
|
30
|
+
&& version.matchRegex
|
|
31
|
+
&& !version.apply
|
|
32
|
+
&& parseInt(version.number) < number;
|
|
30
33
|
|
|
31
|
-
if (
|
|
34
|
+
if (shouldArchive)
|
|
32
35
|
oldVersions.push(versionDir);
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -46,9 +49,9 @@ class Clean extends Command {
|
|
|
46
49
|
path.join(archiveDir, oldVersion)
|
|
47
50
|
);
|
|
48
51
|
|
|
49
|
-
console.log(`Old versions
|
|
52
|
+
console.log(`Old versions archived: ${oldVersions.length}`);
|
|
50
53
|
} else
|
|
51
|
-
console.log(`No versions to
|
|
54
|
+
console.log(`No versions to archive.`);
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
package/myt-push.js
CHANGED
|
@@ -90,17 +90,17 @@ class Push extends Command {
|
|
|
90
90
|
|
|
91
91
|
// Get database version
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
const dbVersion = await myt.fetchDbVersion() || {};
|
|
94
94
|
|
|
95
95
|
console.log(
|
|
96
96
|
`Database information:`
|
|
97
|
-
+ `\n -> Version: ${
|
|
98
|
-
+ `\n -> Commit: ${
|
|
97
|
+
+ `\n -> Version: ${dbVersion.number}`
|
|
98
|
+
+ `\n -> Commit: ${dbVersion.gitCommit}`
|
|
99
99
|
);
|
|
100
100
|
|
|
101
|
-
if (!
|
|
102
|
-
|
|
103
|
-
if (!/^[0-9]*$/.test(
|
|
101
|
+
if (!dbVersion.number)
|
|
102
|
+
dbVersion.number = String('0').padStart(opts.versionDigits, '0');
|
|
103
|
+
if (!/^[0-9]*$/.test(dbVersion.number))
|
|
104
104
|
throw new Error('Wrong database version');
|
|
105
105
|
|
|
106
106
|
// Prevent push to production by mistake
|
|
@@ -142,15 +142,24 @@ class Push extends Command {
|
|
|
142
142
|
let silent = true;
|
|
143
143
|
const versionsDir = opts.versionsDir;
|
|
144
144
|
|
|
145
|
-
function logVersion(version, name, error) {
|
|
146
|
-
|
|
145
|
+
function logVersion(version, name, action, error) {
|
|
146
|
+
let actionMsg;
|
|
147
|
+
switch(action) {
|
|
148
|
+
case 'apply':
|
|
149
|
+
actionMsg = '[A]'.green;
|
|
150
|
+
break;
|
|
151
|
+
case 'ignore':
|
|
152
|
+
actionMsg = '[I]'.blue;
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
actionMsg = '[W]'.yellow;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log('', (actionMsg + version).bold, name);
|
|
147
159
|
}
|
|
148
160
|
function logScript(type, message, error) {
|
|
149
161
|
console.log(' ', type.bold, message);
|
|
150
162
|
}
|
|
151
|
-
function isUndoScript(script) {
|
|
152
|
-
return /\.undo\.sql$/.test(script);
|
|
153
|
-
}
|
|
154
163
|
|
|
155
164
|
const skipFiles = new Set([
|
|
156
165
|
'README.md',
|
|
@@ -159,78 +168,49 @@ class Push extends Command {
|
|
|
159
168
|
|
|
160
169
|
if (await fs.pathExists(versionsDir)) {
|
|
161
170
|
const versionDirs = await fs.readdir(versionsDir);
|
|
162
|
-
|
|
163
171
|
for (const versionDir of versionDirs) {
|
|
164
172
|
if (skipFiles.has(versionDir)) continue;
|
|
173
|
+
const version = await myt.loadVersion(versionDir);
|
|
165
174
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
logVersion('[?????]'.yellow, versionDir,
|
|
175
|
+
if (!version) {
|
|
176
|
+
logVersion('[?????]'.yellow, versionDir, 'warn',
|
|
169
177
|
`Wrong directory name.`
|
|
170
178
|
);
|
|
171
179
|
continue;
|
|
172
180
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (versionNumber.length != version.number.length) {
|
|
178
|
-
logVersion('[*****]'.gray, versionDir,
|
|
179
|
-
`Bad version length, should have ${version.number.length} characters.`
|
|
181
|
+
if (version.number.length != dbVersion.number.length) {
|
|
182
|
+
logVersion('[*****]'.gray, versionDir, 'warn'
|
|
183
|
+
`Bad version length, should have ${dbVersion.number.length} characters.`
|
|
180
184
|
);
|
|
181
185
|
continue;
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const [versionLog] = await conn.query(
|
|
188
|
-
`SELECT file FROM versionLog
|
|
189
|
-
WHERE code = ?
|
|
190
|
-
AND number = ?
|
|
191
|
-
AND errorNumber IS NULL`,
|
|
192
|
-
[opts.code, versionNumber]
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
for (const script of scripts)
|
|
196
|
-
if (!isUndoScript(script)
|
|
197
|
-
&& versionLog.findIndex(x => x.file == script) === -1) {
|
|
198
|
-
silent = false;
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
|
|
188
|
+
const {apply} = version;
|
|
189
|
+
if (apply) silent = false;
|
|
202
190
|
if (silent) continue;
|
|
203
|
-
logVersion(`[${versionNumber}]`.cyan, versionName);
|
|
204
191
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
192
|
+
const action = apply ? 'apply' : 'ignore';
|
|
193
|
+
logVersion(`[${version.number}]`.cyan, version.name, action);
|
|
194
|
+
|
|
195
|
+
if (!apply) continue;
|
|
196
|
+
|
|
197
|
+
for (const script of version.scripts) {
|
|
198
|
+
const scriptFile = script.file;
|
|
199
|
+
|
|
200
|
+
if (!script.matchRegex) {
|
|
201
|
+
logScript('[W]'.yellow, scriptFile, `Wrong file name.`);
|
|
208
202
|
continue;
|
|
209
203
|
}
|
|
210
|
-
if (isUndoScript(script))
|
|
211
|
-
continue;
|
|
212
204
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
AND file = ?`,
|
|
218
|
-
[
|
|
219
|
-
opts.code,
|
|
220
|
-
versionNumber,
|
|
221
|
-
script
|
|
222
|
-
]
|
|
223
|
-
);
|
|
224
|
-
const apply = !row || row.errorNumber;
|
|
225
|
-
const actionMsg = apply ? '[+]'.green : '[I]'.blue;
|
|
226
|
-
|
|
227
|
-
logScript(actionMsg, script);
|
|
228
|
-
if (!apply) continue;
|
|
205
|
+
const actionMsg = script.apply ? '[+]'.green : '[I]'.blue;
|
|
206
|
+
logScript(actionMsg, scriptFile);
|
|
207
|
+
|
|
208
|
+
if (!script.apply) continue;
|
|
229
209
|
|
|
230
210
|
let err;
|
|
231
211
|
try {
|
|
232
212
|
await connExt.queryFromFile(pushConn,
|
|
233
|
-
`${
|
|
213
|
+
`${versionsDir}/${versionDir}/${scriptFile}`);
|
|
234
214
|
} catch (e) {
|
|
235
215
|
err = e;
|
|
236
216
|
}
|
|
@@ -251,8 +231,8 @@ class Push extends Command {
|
|
|
251
231
|
errorMessage = VALUES(errorMessage)`,
|
|
252
232
|
[
|
|
253
233
|
opts.code,
|
|
254
|
-
|
|
255
|
-
|
|
234
|
+
version.number,
|
|
235
|
+
scriptFile,
|
|
256
236
|
err && err.errno,
|
|
257
237
|
err && err.message
|
|
258
238
|
]
|
|
@@ -262,7 +242,7 @@ class Push extends Command {
|
|
|
262
242
|
nChanges++;
|
|
263
243
|
}
|
|
264
244
|
|
|
265
|
-
await this.updateVersion('number',
|
|
245
|
+
await this.updateVersion('number', version.number);
|
|
266
246
|
}
|
|
267
247
|
}
|
|
268
248
|
|
|
@@ -273,7 +253,7 @@ class Push extends Command {
|
|
|
273
253
|
const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
|
|
274
254
|
|
|
275
255
|
let nRoutines = 0;
|
|
276
|
-
const changes = await this.changedRoutines(
|
|
256
|
+
const changes = await this.changedRoutines(dbVersion.gitCommit);
|
|
277
257
|
|
|
278
258
|
const routines = [];
|
|
279
259
|
for (const change of changes)
|
|
@@ -350,7 +330,7 @@ class Push extends Command {
|
|
|
350
330
|
|
|
351
331
|
const typeMsg = `[${change.type.abbr}]`[change.type.color];
|
|
352
332
|
console.log('',
|
|
353
|
-
(
|
|
333
|
+
(actionMsg + statusMsg).bold,
|
|
354
334
|
typeMsg.bold,
|
|
355
335
|
change.fullName
|
|
356
336
|
);
|
|
@@ -410,7 +390,7 @@ class Push extends Command {
|
|
|
410
390
|
const repo = await nodegit.Repository.open(this.opts.workspace);
|
|
411
391
|
const head = await repo.getHeadCommit();
|
|
412
392
|
|
|
413
|
-
if (head &&
|
|
393
|
+
if (head && dbVersion.gitCommit !== head.sha())
|
|
414
394
|
await this.updateVersion('gitCommit', head.sha());
|
|
415
395
|
}
|
|
416
396
|
|
package/myt-run.js
CHANGED
package/myt-version.js
CHANGED
|
@@ -74,9 +74,9 @@ class Version extends Command {
|
|
|
74
74
|
const versionNames = new Set();
|
|
75
75
|
const versionDirs = await fs.readdir(opts.versionsDir);
|
|
76
76
|
for (const versionDir of versionDirs) {
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
79
|
-
versionNames.add(
|
|
77
|
+
const version = myt.parseVersionDir(versionDir);
|
|
78
|
+
if (!version) continue;
|
|
79
|
+
versionNames.add(version.name);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
if (!versionName) {
|
package/myt.js
CHANGED
|
@@ -11,6 +11,8 @@ const mysql = require('mysql2/promise');
|
|
|
11
11
|
const nodegit = require('nodegit');
|
|
12
12
|
const camelToSnake = require('./lib/util').camelToSnake;
|
|
13
13
|
|
|
14
|
+
const scriptRegex = /^[0-9]{2}-[a-zA-Z0-9_]+(?:\.(?!undo)([a-zA-Z0-9_]+))?(\.undo)?\.sql$/;
|
|
15
|
+
|
|
14
16
|
class Myt {
|
|
15
17
|
static usage = {
|
|
16
18
|
description: 'Utility for database versioning',
|
|
@@ -172,7 +174,7 @@ class Myt {
|
|
|
172
174
|
|
|
173
175
|
const defaultConfig = require(`${__dirname}/assets/myt.default.yml`);
|
|
174
176
|
const config = Object.assign({}, defaultConfig);
|
|
175
|
-
|
|
177
|
+
|
|
176
178
|
const configFile = 'myt.config.yml';
|
|
177
179
|
const configPath = path.join(opts.workspace, configFile);
|
|
178
180
|
|
|
@@ -205,7 +207,7 @@ class Myt {
|
|
|
205
207
|
opts.dumpDir = path.join(opts.mytDir, 'dump');
|
|
206
208
|
|
|
207
209
|
// Database configuration
|
|
208
|
-
|
|
210
|
+
|
|
209
211
|
let iniDir = path.join(__dirname, 'assets');
|
|
210
212
|
let iniFile = 'db.ini';
|
|
211
213
|
|
|
@@ -214,10 +216,10 @@ class Myt {
|
|
|
214
216
|
iniFile = `${opts.remote}.ini`;
|
|
215
217
|
}
|
|
216
218
|
const iniPath = path.join(iniDir, iniFile);
|
|
217
|
-
|
|
219
|
+
|
|
218
220
|
if (!await fs.pathExists(iniPath))
|
|
219
221
|
throw new Error(`Database config file not found: ${iniPath}`);
|
|
220
|
-
|
|
222
|
+
|
|
221
223
|
let dbConfig;
|
|
222
224
|
try {
|
|
223
225
|
const iniData = ini.parse(await fs.readFile(iniPath, 'utf8')).client;
|
|
@@ -299,12 +301,17 @@ class Myt {
|
|
|
299
301
|
AND TABLE_NAME = 'version'`,
|
|
300
302
|
[opts.versionSchema]
|
|
301
303
|
);
|
|
302
|
-
|
|
304
|
+
|
|
303
305
|
if (!res.tableExists) {
|
|
304
306
|
const structure = await fs.readFile(
|
|
305
307
|
`${__dirname}/assets/structure.sql`, 'utf8');
|
|
306
308
|
await conn.query(structure);
|
|
307
309
|
}
|
|
310
|
+
|
|
311
|
+
const [[realm]] = await conn.query(
|
|
312
|
+
`SELECT realm FROM versionConfig`
|
|
313
|
+
);
|
|
314
|
+
this.realm = realm;
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
return this.conn;
|
|
@@ -332,6 +339,58 @@ class Myt {
|
|
|
332
339
|
};
|
|
333
340
|
}
|
|
334
341
|
|
|
342
|
+
async loadVersion(versionDir) {
|
|
343
|
+
const {opts} = this;
|
|
344
|
+
|
|
345
|
+
const info = this.parseVersionDir(versionDir);
|
|
346
|
+
if (!info) return null;
|
|
347
|
+
|
|
348
|
+
const versionsDir = opts.versionsDir;
|
|
349
|
+
const scriptsDir = `${versionsDir}/${versionDir}`;
|
|
350
|
+
const scriptList = await fs.readdir(scriptsDir);
|
|
351
|
+
|
|
352
|
+
const [res] = await this.conn.query(
|
|
353
|
+
`SELECT file, errorNumber IS NOT NULL hasError
|
|
354
|
+
FROM versionLog
|
|
355
|
+
WHERE code = ?
|
|
356
|
+
AND number = ?`,
|
|
357
|
+
[opts.code, info.number]
|
|
358
|
+
);
|
|
359
|
+
const versionLog = new Map();
|
|
360
|
+
res.map(x => versionLog.set(x.file, x));
|
|
361
|
+
|
|
362
|
+
let applyVersion = false;
|
|
363
|
+
const scripts = [];
|
|
364
|
+
|
|
365
|
+
for (const file of scriptList) {
|
|
366
|
+
const match = file.match(scriptRegex);
|
|
367
|
+
if (match) {
|
|
368
|
+
const scriptRealm = match[1];
|
|
369
|
+
const isUndo = !!match[2];
|
|
370
|
+
|
|
371
|
+
if ((scriptRealm && scriptRealm !== this.realm) || isUndo)
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const logInfo = versionLog.get(file);
|
|
376
|
+
const apply = !logInfo || logInfo.hasError;
|
|
377
|
+
if (apply) applyVersion = true;
|
|
378
|
+
|
|
379
|
+
scripts.push({
|
|
380
|
+
file,
|
|
381
|
+
matchRegex: !!match,
|
|
382
|
+
apply
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
number: info.number,
|
|
388
|
+
name: info.name,
|
|
389
|
+
scripts,
|
|
390
|
+
apply: applyVersion
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
335
394
|
async openRepo() {
|
|
336
395
|
const {opts} = this;
|
|
337
396
|
|