@dbcube/cli 5.1.4 โ†’ 5.1.5

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.
@@ -0,0 +1,125 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Migration history for .alter.cube files.
7
+ * Stored in dbcube/migrations.json so the history travels with the repo.
8
+ */
9
+ const HISTORY_PATH = () => path.join(process.cwd(), 'dbcube', 'migrations.json');
10
+
11
+ function loadHistory() {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(HISTORY_PATH(), 'utf8'));
14
+ } catch {
15
+ return { applied: [], lastBatch: 0 };
16
+ }
17
+ }
18
+
19
+ function saveHistory(history) {
20
+ fs.mkdirSync(path.dirname(HISTORY_PATH()), { recursive: true });
21
+ fs.writeFileSync(HISTORY_PATH(), JSON.stringify(history, null, 2), 'utf8');
22
+ }
23
+
24
+ function hashFile(filePath) {
25
+ return crypto.createHash('sha1').update(fs.readFileSync(filePath)).digest('hex');
26
+ }
27
+
28
+ function collectAlterFiles(dir = path.join(process.cwd(), 'dbcube'), acc = []) {
29
+ if (!fs.existsSync(dir)) return acc;
30
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
31
+ const full = path.join(dir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ if (!['triggers', 'logs', 'node_modules', 'applied'].includes(entry.name)) collectAlterFiles(full, acc);
34
+ } else if (entry.name.endsWith('.alter.cube')) {
35
+ acc.push(full);
36
+ }
37
+ }
38
+ return acc;
39
+ }
40
+
41
+ /** Splits an .alter.cube into header (@database/@table) + directive blocks. */
42
+ function parseAlterFile(content) {
43
+ const header = [];
44
+ const dbMatch = content.match(/@database\s*\([^)]*\)\s*;/);
45
+ const tableMatch = content.match(/@table\s*\([^)]*\)\s*;/);
46
+ if (dbMatch) header.push(dbMatch[0]);
47
+ if (tableMatch) header.push(tableMatch[0]);
48
+
49
+ const directives = [];
50
+ const regex = /@(changeName|addColumn|deleteColumn|renameColumn|changeType|changeLength|changeDefault|changeOptions|changeEnumValues)\s*\(([\s\S]*?)\)\s*;/g;
51
+ let m;
52
+ while ((m = regex.exec(content)) !== null) {
53
+ directives.push({ name: m[1], arg: m[2].trim(), raw: m[0] });
54
+ }
55
+ return { header, directives };
56
+ }
57
+
58
+ function swapLastNew(arg) {
59
+ // Swaps every {last: "x"; new: "y"} pair inside the directive argument
60
+ return arg.replace(
61
+ /last\s*:\s*"([^"]*)"\s*;\s*new\s*:\s*"([^"]*)"/g,
62
+ (_, last, next) => `last: "${next}"; new: "${last}"`
63
+ );
64
+ }
65
+
66
+ /** Inverts one directive. Returns a string (reverse cube code) or null if irreversible. */
67
+ function invertDirective(d) {
68
+ switch (d.name) {
69
+ case 'changeName':
70
+ case 'renameColumn':
71
+ case 'changeType':
72
+ case 'changeLength':
73
+ case 'changeDefault':
74
+ return `@${d.name}(${swapLastNew(d.arg)});`;
75
+ case 'addColumn': {
76
+ // Reverse of adding columns = deleting them
77
+ const cols = [...d.arg.matchAll(/^\s*(\w+)\s*:\s*\{/gm)].map(m => m[1]);
78
+ // Top-level columns only: the first identifier after { or after a top-level };
79
+ const topCols = [...d.arg.matchAll(/(?:^|\};)\s*(\w+)\s*:\s*\{/g)].map(m => m[1]);
80
+ const names = topCols.length > 0 ? topCols : cols;
81
+ if (names.length === 0) return null;
82
+ return names.map(c => `@deleteColumn("${c}");`).join('\n');
83
+ }
84
+ case 'changeOptions': {
85
+ // Swap add/remove arrays
86
+ const swapped = d.arg
87
+ .replace(/\badd\s*:/g, '__TMP__:')
88
+ .replace(/\bremove\s*:/g, 'add:')
89
+ .replace(/__TMP__:/g, 'remove:');
90
+ return `@changeOptions(${swapped});`;
91
+ }
92
+ case 'deleteColumn':
93
+ case 'changeEnumValues':
94
+ return null; // irreversible: dropped data / unknown previous values
95
+ default:
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Builds the full reverse .alter.cube content for a file.
102
+ * Returns { reverse: string | null, irreversible: string[] }.
103
+ */
104
+ function buildReverse(content) {
105
+ const { header, directives } = parseAlterFile(content);
106
+ const irreversible = [];
107
+ const reversed = [];
108
+
109
+ for (const d of [...directives].reverse()) {
110
+ const inv = invertDirective(d);
111
+ if (inv === null) {
112
+ irreversible.push(`@${d.name}`);
113
+ } else {
114
+ reversed.push(inv);
115
+ }
116
+ }
117
+
118
+ if (irreversible.length > 0) return { reverse: null, irreversible };
119
+ return {
120
+ reverse: header.join('\n') + '\n\n' + reversed.join('\n\n') + '\n',
121
+ irreversible: [],
122
+ };
123
+ }
124
+
125
+ module.exports = { loadHistory, saveHistory, hashFile, collectAlterFiles, buildReverse };
@@ -0,0 +1,72 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { Schema } = require('@dbcube/schema-builder');
5
+ const ConfigFileUtils = require('../../utils/ConfigFileUtils');
6
+ const { loadHistory, saveHistory } = require('./lib');
7
+
8
+ /**
9
+ * dbcube migrate:rollback โ€” Reverts the last applied batch of .alter.cube
10
+ * migrations using the auto-generated reverse directives stored at apply time.
11
+ */
12
+ async function main() {
13
+ const history = loadHistory();
14
+ if (history.applied.length === 0 || history.lastBatch === 0) {
15
+ console.log(`\n${chalk.yellow('โš ')} Nothing to rollback โ€” no applied migrations.\n`);
16
+ return;
17
+ }
18
+
19
+ const batch = history.lastBatch;
20
+ const toRevert = history.applied.filter(a => a.batch === batch);
21
+
22
+ const blocked = toRevert.filter(a => !a.reverse);
23
+ if (blocked.length > 0) {
24
+ console.error(`\n${chalk.red('โŒ Cannot rollback batch')} ${batch}:`);
25
+ for (const b of blocked) {
26
+ console.error(` ${b.file} ${chalk.gray('contains irreversible directives (@deleteColumn / @changeEnumValues)')}`);
27
+ }
28
+ console.error(`\n${chalk.gray('Write a manual .alter.cube to undo those changes.')}\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ console.log(`\nโช ${chalk.green.bold(`Rolling back batch ${batch}`)} (${toRevert.length} migration(s))\n`);
33
+
34
+ const cubesDir = path.join(process.cwd(), 'dbcube');
35
+ const tempFiles = [];
36
+
37
+ try {
38
+ // Write reverse files (reverse application order: last applied reverts first)
39
+ for (const entry of [...toRevert].reverse()) {
40
+ const tempName = `__rollback_${Date.now()}_${entry.file}`;
41
+ const tempPath = path.join(cubesDir, tempName);
42
+ fs.writeFileSync(tempPath, entry.reverse, 'utf8');
43
+ tempFiles.push({ tempName, tempPath, entry });
44
+ }
45
+
46
+ const configuredDatabases = await ConfigFileUtils.getConfiguredDatabases();
47
+ for (const config of configuredDatabases) {
48
+ const schema = new Schema(config.name);
49
+ try {
50
+ await schema.executeAlters(tempFiles.map(t => t.tempName));
51
+ } catch (e) {
52
+ if (!String(e.message).includes('no .alter.cube files')) throw e;
53
+ }
54
+ }
55
+
56
+ // Remove reverted entries from history
57
+ history.applied = history.applied.filter(a => a.batch !== batch);
58
+ history.lastBatch = history.applied.reduce((max, a) => Math.max(max, a.batch), 0);
59
+ saveHistory(history);
60
+
61
+ console.log(`\n${chalk.green('โœ“')} Batch ${batch} rolled back. ${chalk.gray('Remember to update your .table.cube files if they reflected those changes.')}\n`);
62
+ } finally {
63
+ for (const t of tempFiles) {
64
+ try { fs.unlinkSync(t.tempPath); } catch { /* already gone */ }
65
+ }
66
+ }
67
+ }
68
+
69
+ main().catch(error => {
70
+ console.error(`${chalk.red('โŒ Rollback failed:')}`, error.message);
71
+ process.exit(1);
72
+ });
@@ -0,0 +1,55 @@
1
+ const path = require('path');
2
+ const chalk = require('chalk');
3
+ const { loadHistory, hashFile, collectAlterFiles } = require('./lib');
4
+
5
+ /**
6
+ * dbcube migrate:status โ€” Shows applied and pending .alter.cube migrations.
7
+ */
8
+ async function main() {
9
+ const history = loadHistory();
10
+ const files = collectAlterFiles();
11
+ const appliedByFile = new Map(history.applied.map(a => [a.file, a]));
12
+
13
+ console.log(`\n๐Ÿ“‹ ${chalk.green.bold('Migration status')}\n`);
14
+
15
+ if (history.applied.length === 0 && files.length === 0) {
16
+ console.log(` ${chalk.gray('No .alter.cube migrations found.')}\n`);
17
+ return;
18
+ }
19
+
20
+ if (history.applied.length > 0) {
21
+ console.log(chalk.cyan.bold(' Applied:'));
22
+ for (const a of history.applied) {
23
+ const reversible = a.reverse ? chalk.gray('reversible') : chalk.yellow('irreversible');
24
+ console.log(` ${chalk.green('โœ“')} ${a.file} ${chalk.gray(`batch ${a.batch} ยท ${a.applied_at}`)} ยท ${reversible}`);
25
+ }
26
+ }
27
+
28
+ const pending = [];
29
+ const modified = [];
30
+ for (const f of files) {
31
+ const base = path.basename(f);
32
+ const applied = appliedByFile.get(base);
33
+ if (!applied) pending.push(base);
34
+ else if (applied.hash !== hashFile(f)) modified.push(base);
35
+ }
36
+
37
+ if (pending.length > 0) {
38
+ console.log(`\n${chalk.cyan.bold(' Pending:')}`);
39
+ for (const p of pending) console.log(` ${chalk.yellow('โ—‹')} ${p}`);
40
+ console.log(`\n ${chalk.gray('Apply with:')} ${chalk.white('npx dbcube run table:alter')}`);
41
+ } else {
42
+ console.log(`\n ${chalk.green('Everything up to date โ€” no pending migrations.')}`);
43
+ }
44
+
45
+ if (modified.length > 0) {
46
+ console.log(`\n${chalk.red.bold(' โš  Modified after being applied:')}`);
47
+ for (const m of modified) console.log(` ${chalk.red('!')} ${m} ${chalk.gray('โ€” create a NEW alter file instead of editing applied ones')}`);
48
+ }
49
+ console.log('');
50
+ }
51
+
52
+ main().catch(error => {
53
+ console.error('Error fatal:', error);
54
+ process.exit(1);
55
+ });
@@ -0,0 +1,192 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const ConfigFileUtils = require('../../utils/ConfigFileUtils');
5
+ const { Engine } = require('@dbcube/schema-builder');
6
+
7
+ /**
8
+ * dbcube run pull [database] โ€” Introspects an EXISTING database and generates
9
+ * .table.cube files from its real schema. The migration path for legacy projects.
10
+ */
11
+ const SQL_TYPE_MAP = {
12
+ 'int': 'int', 'integer': 'int', 'bigint': 'bigint', 'tinyint': 'tinyint', 'smallint': 'int',
13
+ 'character varying': 'varchar', 'varchar': 'varchar', 'char': 'varchar', 'character': 'varchar',
14
+ 'text': 'text', 'longtext': 'text', 'mediumtext': 'text',
15
+ 'decimal': 'decimal', 'numeric': 'decimal', 'float': 'float', 'real': 'float',
16
+ 'double': 'double', 'double precision': 'double',
17
+ 'boolean': 'boolean', 'bool': 'boolean',
18
+ 'date': 'date', 'datetime': 'datetime',
19
+ 'timestamp': 'timestamp', 'timestamp without time zone': 'timestamp', 'timestamp with time zone': 'timestamp',
20
+ 'json': 'json', 'jsonb': 'json', 'enum': 'enum', 'uuid': 'varchar', 'blob': 'text',
21
+ };
22
+
23
+ async function raw(engine, sql) {
24
+ const res = await engine.run('query_engine', ['--action', 'raw', '--query', sql, '--params', '[]']);
25
+ if (res.status !== 200) throw new Error(res.message);
26
+ return res.data || [];
27
+ }
28
+
29
+ async function introspect(engine, dbName, motor) {
30
+ const tables = {}; // name -> { columns: [], pks: Set, fks: {col: {table, column}} }
31
+
32
+ if (motor === 'sqlite') {
33
+ const rows = await raw(engine, `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'dbcube_%'`);
34
+ for (const r of rows) {
35
+ const t = r.name;
36
+ const cols = await raw(engine, `PRAGMA table_info(${t})`);
37
+ const fkRows = await raw(engine, `PRAGMA foreign_key_list(${t})`);
38
+ const fks = {};
39
+ for (const fk of fkRows) fks[fk.from] = { table: fk.table, column: fk.to || 'id' };
40
+ tables[t] = {
41
+ columns: cols.map(c => ({
42
+ name: c.name,
43
+ dataType: String(c.type || 'text').toLowerCase().replace(/\(.*\)/, '').trim(),
44
+ length: (String(c.type || '').match(/\((\d+(,\d+)?)\)/) || [])[1] || null,
45
+ nullable: c.notnull == 0,
46
+ defaultValue: c.dflt_value,
47
+ isPk: c.pk > 0,
48
+ autoinc: c.pk > 0 && /int/i.test(String(c.type)),
49
+ enumValues: null,
50
+ })),
51
+ fks,
52
+ };
53
+ }
54
+ } else if (motor === 'mysql') {
55
+ const cols = await raw(engine, `SELECT TABLE_NAME tn, COLUMN_NAME cn, DATA_TYPE dt, CHARACTER_MAXIMUM_LENGTH len, NUMERIC_PRECISION np, NUMERIC_SCALE ns, IS_NULLABLE nullable, COLUMN_DEFAULT dv, COLUMN_KEY ck, EXTRA extra, COLUMN_TYPE ct FROM information_schema.columns WHERE table_schema = DATABASE() ORDER BY TABLE_NAME, ORDINAL_POSITION`);
56
+ const fkRows = await raw(engine, `SELECT TABLE_NAME tn, COLUMN_NAME cn, REFERENCED_TABLE_NAME rt, REFERENCED_COLUMN_NAME rc FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL`);
57
+ for (const c of cols) {
58
+ const t = c.tn;
59
+ if (String(t).startsWith('dbcube_')) continue;
60
+ tables[t] = tables[t] || { columns: [], fks: {} };
61
+ let enumValues = null;
62
+ if (c.dt === 'enum' && c.ct) {
63
+ enumValues = [...String(c.ct).matchAll(/'((?:[^']|'')*)'/g)].map(m => m[1]);
64
+ }
65
+ tables[t].columns.push({
66
+ name: c.cn,
67
+ dataType: String(c.dt).toLowerCase(),
68
+ length: c.len ?? (c.np != null && c.ns != null && c.dt === 'decimal' ? `${c.np},${c.ns}` : null),
69
+ nullable: c.nullable === 'YES',
70
+ defaultValue: c.dv,
71
+ isPk: c.ck === 'PRI',
72
+ autoinc: /auto_increment/i.test(c.extra || ''),
73
+ enumValues,
74
+ });
75
+ }
76
+ for (const fk of fkRows) {
77
+ if (!tables[fk.tn]) continue;
78
+ tables[fk.tn].fks[fk.cn] = { table: fk.rt, column: fk.rc };
79
+ }
80
+ } else if (motor === 'postgres' || motor === 'postgresql') {
81
+ const cols = await raw(engine, `SELECT c.table_name tn, c.column_name cn, c.data_type dt, c.character_maximum_length len, c.numeric_precision np, c.numeric_scale ns, c.is_nullable nullable, c.column_default dv FROM information_schema.columns c WHERE c.table_schema = 'public' ORDER BY c.table_name, c.ordinal_position`);
82
+ const pkRows = await raw(engine, `SELECT kcu.table_name tn, kcu.column_name cn FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'`);
83
+ const fkRows = await raw(engine, `SELECT kcu.table_name tn, kcu.column_name cn, ccu.table_name rt, ccu.column_name rc FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`);
84
+ const pks = new Set(pkRows.map(r => `${r.tn}.${r.cn}`));
85
+ for (const c of cols) {
86
+ const t = c.tn;
87
+ if (String(t).startsWith('dbcube_')) continue;
88
+ tables[t] = tables[t] || { columns: [], fks: {} };
89
+ tables[t].columns.push({
90
+ name: c.cn,
91
+ dataType: String(c.dt).toLowerCase(),
92
+ length: c.len ?? (c.np != null && c.ns != null && /numeric|decimal/.test(c.dt) ? `${c.np},${c.ns}` : null),
93
+ nullable: c.nullable === 'YES',
94
+ defaultValue: c.dv,
95
+ isPk: pks.has(`${t}.${c.cn}`),
96
+ autoinc: /nextval\(/.test(c.dv || ''),
97
+ enumValues: null,
98
+ });
99
+ }
100
+ for (const fk of fkRows) {
101
+ if (!tables[fk.tn]) continue;
102
+ tables[fk.tn].fks[fk.cn] = { table: fk.rt, column: fk.rc };
103
+ }
104
+ } else {
105
+ throw new Error(`pull is not supported yet for motor '${motor}' (mysql, postgresql, sqlite available)`);
106
+ }
107
+
108
+ return tables;
109
+ }
110
+
111
+ function renderCube(dbName, tableName, table) {
112
+ let out = `@database("${dbName}");\n\n`;
113
+ out += `@meta({\n name: "${tableName}";\n description: "Imported from existing database by dbcube pull";\n});\n\n`;
114
+ out += `@columns({\n`;
115
+
116
+ for (const col of table.columns) {
117
+ const cubeType = SQL_TYPE_MAP[col.dataType] || 'varchar';
118
+ out += ` ${col.name}: {\n`;
119
+ out += ` type: "${cubeType === 'enum' && !col.enumValues ? 'varchar' : cubeType}";\n`;
120
+ if (col.length && ['varchar', 'decimal'].includes(cubeType)) {
121
+ out += ` length: "${col.length}";\n`;
122
+ } else if (cubeType === 'varchar' && !col.length) {
123
+ out += ` length: "255";\n`;
124
+ }
125
+ if (cubeType === 'enum' && col.enumValues && col.enumValues.length > 0) {
126
+ out += ` enumValues: [${col.enumValues.map(v => `"${v}"`).join(', ')}];\n`;
127
+ }
128
+ const options = [];
129
+ if (col.isPk) options.push('"primary"');
130
+ if (col.autoinc) options.push('"autoincrement"');
131
+ if (!col.nullable && !col.isPk) options.push('"not null"');
132
+ if (options.length > 0) out += ` options: [${options.join(', ')}];\n`;
133
+ if (col.defaultValue != null && !col.autoinc && !/nextval|CURRENT_TIMESTAMP/i.test(String(col.defaultValue))) {
134
+ const dv = String(col.defaultValue).replace(/^'(.*)'(::.*)?$/, '$1');
135
+ out += ` defaultValue: "${dv}";\n`;
136
+ }
137
+ const fk = table.fks[col.name];
138
+ if (fk) {
139
+ out += ` foreign: {\n table: "${fk.table}";\n column: "${fk.column}";\n };\n`;
140
+ }
141
+ out += ` };\n`;
142
+ }
143
+ out += `});\n`;
144
+ return out;
145
+ }
146
+
147
+ async function main() {
148
+ const targetDb = process.argv[2];
149
+ const configured = await ConfigFileUtils.getConfiguredDatabases();
150
+ const dbs = targetDb ? configured.filter(d => d.name === targetDb) : configured;
151
+
152
+ if (dbs.length === 0) {
153
+ console.error(`${chalk.red('โŒ')} ${targetDb ? `Database '${targetDb}' is not configured` : 'No databases configured'} in dbcube.config.js`);
154
+ process.exit(1);
155
+ }
156
+
157
+ const cubesDir = path.join(process.cwd(), 'dbcube');
158
+ fs.mkdirSync(cubesDir, { recursive: true });
159
+
160
+ for (const db of dbs) {
161
+ console.log(`\n๐Ÿ“ฅ ${chalk.green.bold('Pulling schema from')} ${chalk.bold(db.name)} (${db.type})...\n`);
162
+ const engine = new Engine(db.name);
163
+ const tables = await introspect(engine, db.name, db.type);
164
+ const names = Object.keys(tables);
165
+
166
+ if (names.length === 0) {
167
+ console.log(` ${chalk.yellow('โš ')} No tables found`);
168
+ continue;
169
+ }
170
+
171
+ for (const t of names) {
172
+ const cube = renderCube(db.name, t, tables[t]);
173
+ const file = path.join(cubesDir, `${t}.table.cube`);
174
+ if (fs.existsSync(file)) {
175
+ const backup = file + '.pulled';
176
+ fs.writeFileSync(backup, cube, 'utf8');
177
+ console.log(` ${chalk.yellow('โš ')} ${t}.table.cube exists โ€” wrote ${path.basename(backup)} instead (diff & merge manually)`);
178
+ } else {
179
+ fs.writeFileSync(file, cube, 'utf8');
180
+ console.log(` ${chalk.green('โœ“')} dbcube/${t}.table.cube (${tables[t].columns.length} columns${Object.keys(tables[t].fks).length ? `, ${Object.keys(tables[t].fks).length} FK` : ''})`);
181
+ }
182
+ }
183
+ }
184
+
185
+ console.log(`\n${chalk.green('โœ“ Pull complete.')} Review the generated cubes, then your schema is fully managed by DBCube.\n`);
186
+ process.exit(0);
187
+ }
188
+
189
+ main().catch(error => {
190
+ console.error(`${chalk.red('โŒ Pull failed:')}`, error.message);
191
+ process.exit(1);
192
+ });
@@ -1,13 +1,66 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
1
3
  const { Schema } = require('@dbcube/schema-builder');
2
4
  const ConfigFileUtils = require('./../../../utils/ConfigFileUtils');
5
+ const { loadHistory, saveHistory, hashFile, collectAlterFiles, buildReverse } = require('../../migrate/lib');
3
6
 
7
+ /**
8
+ * dbcube run table:alter [--dry-run] [--all]
9
+ * Applies PENDING .alter.cube migrations (tracked in dbcube/migrations.json).
10
+ * --dry-run: prints the SQL without executing anything.
11
+ * --all: re-runs every alter file, ignoring the history (legacy behavior).
12
+ */
4
13
  async function main() {
14
+ const args = process.argv.slice(2);
15
+ const dryRun = args.includes('--dry-run');
16
+ const runAll = args.includes('--all');
17
+
5
18
  try {
19
+ const history = loadHistory();
20
+ const appliedNames = new Set(history.applied.map(a => a.file));
21
+ const allFiles = collectAlterFiles();
22
+
23
+ // Only pending files (unless --all)
24
+ const targetFiles = runAll ? allFiles : allFiles.filter(f => !appliedNames.has(path.basename(f)));
25
+
26
+ if (targetFiles.length === 0) {
27
+ console.log('\nโœ… No pending migrations โ€” everything up to date.');
28
+ console.log(' (use --all to re-run applied alter files)\n');
29
+ return;
30
+ }
31
+
32
+ const targetNames = targetFiles.map(f => path.basename(f));
6
33
  const configuredDatabases = await ConfigFileUtils.getConfiguredDatabases();
7
34
 
8
35
  for (const config of configuredDatabases) {
9
36
  const schema = new Schema(config.name);
10
- await schema.executeAlters();
37
+ try {
38
+ await schema.executeAlters(targetNames, { dryRun });
39
+ } catch (e) {
40
+ if (!String(e.message).includes('no .alter.cube files')) throw e;
41
+ }
42
+ }
43
+
44
+ // Record history (skip on dry-run)
45
+ if (!dryRun) {
46
+ const batch = history.lastBatch + 1;
47
+ for (const file of targetFiles) {
48
+ const base = path.basename(file);
49
+ if (appliedNames.has(base)) continue;
50
+ const content = fs.readFileSync(file, 'utf8');
51
+ const { reverse } = buildReverse(content);
52
+ history.applied.push({
53
+ file: base,
54
+ hash: hashFile(file),
55
+ batch,
56
+ applied_at: new Date().toISOString(),
57
+ reverse, // null => irreversible
58
+ });
59
+ }
60
+ history.lastBatch = batch;
61
+ saveHistory(history);
62
+ console.log(`\n๐Ÿ“‹ Recorded batch ${batch} in dbcube/migrations.json`);
63
+ console.log(' Status: npx dbcube migrate:status ยท Rollback: npx dbcube migrate:rollback\n');
11
64
  }
12
65
  } catch (error) {
13
66
  if (error.message.includes("reading 'init'")) {
@@ -33,11 +86,10 @@ async function main() {
33
86
  console.error('');
34
87
  process.exit(1);
35
88
  } else {
36
- console.error('Error:', error);
37
89
  console.error('Error:', error.message);
90
+ process.exit(1);
38
91
  }
39
92
  }
40
93
  }
41
94
 
42
- // Ejecutar el ejemplo
43
95
  main().catch(console.error);
@@ -0,0 +1,70 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { CubeValidator } = require('@dbcube/schema-builder');
5
+
6
+ /**
7
+ * dbcube validate โ€” Validates every .cube file in dbcube/ without executing
8
+ * anything. Exit code 1 when any file is invalid (CI-friendly).
9
+ */
10
+ function collectCubeFiles(dir, acc = []) {
11
+ if (!fs.existsSync(dir)) return acc;
12
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
13
+ const full = path.join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ if (!['triggers', 'logs', 'node_modules'].includes(entry.name)) collectCubeFiles(full, acc);
16
+ } else if (entry.name.endsWith('.cube')) {
17
+ acc.push(full);
18
+ }
19
+ }
20
+ return acc;
21
+ }
22
+
23
+ async function main() {
24
+ const cubesDir = path.join(process.cwd(), 'dbcube');
25
+ const files = collectCubeFiles(cubesDir);
26
+
27
+ if (files.length === 0) {
28
+ console.log(`${chalk.yellow('โš ')} No .cube files found in dbcube/`);
29
+ process.exit(0);
30
+ }
31
+
32
+ console.log(`\n๐Ÿ”Ž ${chalk.green('Validating')} ${files.length} cube file(s)...\n`);
33
+
34
+ const validator = new CubeValidator();
35
+ let errorCount = 0;
36
+
37
+ for (const file of files) {
38
+ const rel = path.relative(process.cwd(), file);
39
+ // CubeValidator covers .table.cube structure; other types get a syntax-level pass
40
+ let result;
41
+ try {
42
+ result = validator.validateCubeFile(file);
43
+ } catch (e) {
44
+ result = { isValid: false, errors: [{ error: e.message, lineNumber: 1 }] };
45
+ }
46
+
47
+ if (result.isValid) {
48
+ console.log(` ${chalk.green('โœ“')} ${rel}`);
49
+ } else {
50
+ errorCount++;
51
+ console.log(` ${chalk.red('โœ—')} ${rel}`);
52
+ for (const err of result.errors) {
53
+ const line = err.lineNumber ? chalk.gray(`:${err.lineNumber}`) : '';
54
+ console.log(` ${chalk.red('[error]')} ${err.error}${line}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ console.log('');
60
+ if (errorCount > 0) {
61
+ console.log(`${chalk.red('โœ—')} ${errorCount} file(s) with errors\n`);
62
+ process.exit(1);
63
+ }
64
+ console.log(`${chalk.green('โœ“')} All cube files are valid\n`);
65
+ }
66
+
67
+ main().catch(error => {
68
+ console.error('Error fatal:', error);
69
+ process.exit(1);
70
+ });
package/src/index.js CHANGED
@@ -47,14 +47,26 @@ const commandMap = {
47
47
  'run:database:create:config': '../src/commands/run/database/create/addDatabaseConfig.js',
48
48
  'run:database:create:physical': '../src/commands/run/database/create/createDatabase.js',
49
49
 
50
+ 'run:pull': '../src/commands/run/pull.js',
51
+
50
52
  'run:download': '../src/commands/run/download.js',
51
53
  'run:update': '../src/commands/run/update.js',
52
54
 
55
+ 'init': '../src/commands/init.js',
56
+ 'generate': '../src/commands/generate.js',
57
+ 'validate': '../src/commands/validate.js',
58
+ 'doctor': '../src/commands/doctor.js',
59
+ 'dev': '../src/commands/dev.js',
60
+
61
+ 'migrate:status': '../src/commands/migrate/status.js',
62
+ 'migrate:rollback': '../src/commands/migrate/rollback.js',
63
+
53
64
  'update': '../src/commands/update.js',
54
65
 
55
66
  '--version': '../src/commands/version.js',
56
67
  '-v': '../src/commands/version.js',
57
68
 
69
+ 'help': '../src/commands/help.js',
58
70
  '--help': '../src/commands/help.js',
59
71
  '-h': '../src/commands/help.js',
60
72
  };