@openfactu/cli 0.0.2
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/dist/bin/openfactu.d.ts +2 -0
- package/dist/bin/openfactu.js +23 -0
- package/dist/src/commands/migrate.d.ts +2 -0
- package/dist/src/commands/migrate.js +160 -0
- package/dist/src/commands/plugin.d.ts +2 -0
- package/dist/src/commands/plugin.js +95 -0
- package/dist/src/commands/setup.d.ts +2 -0
- package/dist/src/commands/setup.js +152 -0
- package/dist/src/commands/tenant.d.ts +2 -0
- package/dist/src/commands/tenant.js +220 -0
- package/dist/src/commands/update.d.ts +2 -0
- package/dist/src/commands/update.js +282 -0
- package/dist/src/commands/version.d.ts +2 -0
- package/dist/src/commands/version.js +49 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +24 -0
- package/dist/src/utils/config.d.ts +8 -0
- package/dist/src/utils/config.js +23 -0
- package/dist/src/utils/db.d.ts +12 -0
- package/dist/src/utils/db.js +65 -0
- package/dist/src/utils/logger.d.ts +9 -0
- package/dist/src/utils/logger.js +16 -0
- package/dist/src/utils/paths.d.ts +18 -0
- package/dist/src/utils/paths.js +84 -0
- package/package.json +39 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
// Registrar ts-node para resolver imports de .ts del server
|
|
5
|
+
try {
|
|
6
|
+
require('ts-node').register({
|
|
7
|
+
transpileOnly: true,
|
|
8
|
+
compilerOptions: { module: 'commonjs', esModuleInterop: true },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
// Opción --path para usar desde cualquier directorio
|
|
13
|
+
const pathIdx = process.argv.indexOf('--path');
|
|
14
|
+
if (pathIdx !== -1 && process.argv[pathIdx + 1]) {
|
|
15
|
+
const { setProjectRoot } = require('../src/utils/paths');
|
|
16
|
+
setProjectRoot(process.argv[pathIdx + 1]);
|
|
17
|
+
// Quitar --path y su valor de argv para que commander no los procese
|
|
18
|
+
process.argv.splice(pathIdx, 2);
|
|
19
|
+
}
|
|
20
|
+
const index_1 = require("../src/index");
|
|
21
|
+
const program = (0, index_1.createCLI)();
|
|
22
|
+
program.option('--path <dir>', 'Ruta a la instalación de OpenFactu');
|
|
23
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerMigrateCommand = registerMigrateCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const db_1 = require("../utils/db");
|
|
13
|
+
const logger_1 = require("../utils/logger");
|
|
14
|
+
const MIGRATIONS_DIR = require('../utils/paths').getMigrationsDir();
|
|
15
|
+
function getMigrationFiles() {
|
|
16
|
+
if (!fs_1.default.existsSync(MIGRATIONS_DIR))
|
|
17
|
+
return [];
|
|
18
|
+
return fs_1.default.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort();
|
|
19
|
+
}
|
|
20
|
+
async function getAppliedMigrations(tenantDb, schemaName) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await tenantDb.execute(db_1.sql.raw(`SELECT id FROM "${schemaName}"."_MigrationHistory" ORDER BY id`));
|
|
23
|
+
return result.rows.map((r) => r.id);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function applyMigration(tenantDb, schemaName, file) {
|
|
30
|
+
const migrationId = file.replace('.sql', '');
|
|
31
|
+
const filePath = path_1.default.join(MIGRATIONS_DIR, file);
|
|
32
|
+
let rawSql = fs_1.default.readFileSync(filePath, 'utf8');
|
|
33
|
+
const processedSql = rawSql.replace(/{{schema}}/g, schemaName);
|
|
34
|
+
const statements = processedSql
|
|
35
|
+
.split(/;(?=(?:[^$]*\$\$[^$]*\$\$)*[^$]*$)/)
|
|
36
|
+
.map((s) => s.trim())
|
|
37
|
+
.filter((s) => s.length > 0);
|
|
38
|
+
for (const statement of statements) {
|
|
39
|
+
await tenantDb.execute(db_1.sql.raw(statement));
|
|
40
|
+
}
|
|
41
|
+
await tenantDb.execute(db_1.sql.raw(`INSERT INTO "${schemaName}"."_MigrationHistory" (id, description) VALUES ('${migrationId}', 'Aplicado desde CLI')`));
|
|
42
|
+
}
|
|
43
|
+
function registerMigrateCommand(program) {
|
|
44
|
+
// ── openfactu migrate ──
|
|
45
|
+
program
|
|
46
|
+
.command('migrate')
|
|
47
|
+
.description('Ejecuta migraciones pendientes en todos los tenants')
|
|
48
|
+
.option('-t, --tenant <name>', 'Migrar solo un tenant específico')
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
const spinner = (0, ora_1.default)('Conectando a la base de datos...').start();
|
|
51
|
+
try {
|
|
52
|
+
const allFiles = getMigrationFiles();
|
|
53
|
+
if (allFiles.length === 0) {
|
|
54
|
+
spinner.fail('No se encontraron archivos de migración');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let tenants;
|
|
58
|
+
if (opts.tenant) {
|
|
59
|
+
const tenant = await (0, db_1.getTenantByName)(opts.tenant);
|
|
60
|
+
if (!tenant) {
|
|
61
|
+
spinner.fail(`Tenant "${opts.tenant}" no encontrado`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
tenants = [tenant];
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
tenants = await (0, db_1.getAllTenants)();
|
|
68
|
+
}
|
|
69
|
+
if (tenants.length === 0) {
|
|
70
|
+
spinner.warn('No hay tenants registrados');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
spinner.succeed(`${tenants.length} tenant(s) encontrado(s), ${allFiles.length} migraciones disponibles`);
|
|
74
|
+
logger_1.log.blank();
|
|
75
|
+
let totalApplied = 0;
|
|
76
|
+
for (const tenant of tenants) {
|
|
77
|
+
const tenantDb = (0, db_1.getTenantDb)(tenant.schemaName);
|
|
78
|
+
const applied = await getAppliedMigrations(tenantDb, tenant.schemaName);
|
|
79
|
+
const pending = allFiles.filter((f) => !applied.includes(f.replace('.sql', '')));
|
|
80
|
+
if (pending.length === 0) {
|
|
81
|
+
logger_1.log.dim(` ${tenant.name} (${tenant.schemaName}) — al dia`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
logger_1.log.title(` ${tenant.name} (${tenant.schemaName})`);
|
|
85
|
+
for (const file of pending) {
|
|
86
|
+
const migSpinner = (0, ora_1.default)(` Aplicando ${file}...`).start();
|
|
87
|
+
try {
|
|
88
|
+
await applyMigration(tenantDb, tenant.schemaName, file);
|
|
89
|
+
migSpinner.succeed(` ${file}`);
|
|
90
|
+
totalApplied++;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
migSpinner.fail(` ${file}: ${err.message}`);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
logger_1.log.blank();
|
|
99
|
+
if (totalApplied > 0) {
|
|
100
|
+
logger_1.log.success(`${totalApplied} migración(es) aplicada(s)`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
logger_1.log.success('Todos los tenants están al día');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
spinner.fail(err.message);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await (0, db_1.disconnect)();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// ── openfactu migrate:status ──
|
|
115
|
+
program
|
|
116
|
+
.command('migrate:status')
|
|
117
|
+
.description('Muestra el estado de migraciones por tenant')
|
|
118
|
+
.action(async () => {
|
|
119
|
+
const spinner = (0, ora_1.default)('Leyendo estado de migraciones...').start();
|
|
120
|
+
try {
|
|
121
|
+
const allFiles = getMigrationFiles();
|
|
122
|
+
const tenants = await (0, db_1.getAllTenants)();
|
|
123
|
+
if (tenants.length === 0) {
|
|
124
|
+
spinner.warn('No hay tenants registrados');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
spinner.succeed(`${tenants.length} tenant(s), ${allFiles.length} migraciones`);
|
|
128
|
+
logger_1.log.blank();
|
|
129
|
+
for (const tenant of tenants) {
|
|
130
|
+
const tenantDb = (0, db_1.getTenantDb)(tenant.schemaName);
|
|
131
|
+
const applied = await getAppliedMigrations(tenantDb, tenant.schemaName);
|
|
132
|
+
const pending = allFiles.filter((f) => !applied.includes(f.replace('.sql', '')));
|
|
133
|
+
const table = new cli_table3_1.default({
|
|
134
|
+
head: [chalk_1.default.white('Migración'), chalk_1.default.white('Estado')],
|
|
135
|
+
colWidths: [45, 15],
|
|
136
|
+
style: { head: [], border: ['dim'] },
|
|
137
|
+
});
|
|
138
|
+
for (const file of allFiles) {
|
|
139
|
+
const migId = file.replace('.sql', '');
|
|
140
|
+
const isApplied = applied.includes(migId);
|
|
141
|
+
table.push([
|
|
142
|
+
migId,
|
|
143
|
+
isApplied ? chalk_1.default.green('✓ Aplicada') : chalk_1.default.yellow('Pendiente'),
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk_1.default.bold(`\n ${tenant.name}`) + chalk_1.default.dim(` (${tenant.schemaName})`));
|
|
147
|
+
console.log(chalk_1.default.dim(` ${applied.length} aplicadas, `) +
|
|
148
|
+
(pending.length > 0 ? chalk_1.default.yellow(`${pending.length} pendientes`) : chalk_1.default.green('al día')));
|
|
149
|
+
console.log(table.toString());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
spinner.fail(err.message);
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await (0, db_1.disconnect)();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerPluginCommand = registerPluginCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const db_1 = require("../utils/db");
|
|
13
|
+
const logger_1 = require("../utils/logger");
|
|
14
|
+
const paths_1 = require("../utils/paths");
|
|
15
|
+
const PLUGINS_DIR = (0, paths_1.getPluginsDir)();
|
|
16
|
+
function registerPluginCommand(program) {
|
|
17
|
+
const plugin = program
|
|
18
|
+
.command('plugin')
|
|
19
|
+
.description('Gestión de plugins');
|
|
20
|
+
// ── openfactu plugin list ──
|
|
21
|
+
plugin
|
|
22
|
+
.command('list')
|
|
23
|
+
.description('Lista plugins instalados')
|
|
24
|
+
.action(async () => {
|
|
25
|
+
const spinner = (0, ora_1.default)('Leyendo plugins...').start();
|
|
26
|
+
try {
|
|
27
|
+
// Leer plugins del filesystem
|
|
28
|
+
const installed = [];
|
|
29
|
+
if (fs_1.default.existsSync(PLUGINS_DIR)) {
|
|
30
|
+
const dirs = fs_1.default.readdirSync(PLUGINS_DIR).filter((d) => fs_1.default.statSync(path_1.default.join(PLUGINS_DIR, d)).isDirectory());
|
|
31
|
+
installed.push(...dirs);
|
|
32
|
+
}
|
|
33
|
+
if (installed.length === 0) {
|
|
34
|
+
spinner.warn('No hay plugins instalados');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Leer estado de activación por tenant
|
|
38
|
+
const publicDb = (0, db_1.getPublicDb)();
|
|
39
|
+
let tenantPlugins = [];
|
|
40
|
+
try {
|
|
41
|
+
tenantPlugins = await publicDb.select().from(db_1.schema.tenantPlugins);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Tabla puede no existir aún
|
|
45
|
+
}
|
|
46
|
+
const tenants = await (0, db_1.getAllTenants)();
|
|
47
|
+
spinner.succeed(`${installed.length} plugin(s) instalado(s)`);
|
|
48
|
+
logger_1.log.blank();
|
|
49
|
+
const table = new cli_table3_1.default({
|
|
50
|
+
head: [
|
|
51
|
+
chalk_1.default.white('Plugin'),
|
|
52
|
+
chalk_1.default.white('Manifest'),
|
|
53
|
+
...tenants.map((t) => chalk_1.default.white(t.name)),
|
|
54
|
+
],
|
|
55
|
+
style: { head: [], border: ['dim'] },
|
|
56
|
+
});
|
|
57
|
+
for (const pluginId of installed) {
|
|
58
|
+
const manifestPath = path_1.default.join(PLUGINS_DIR, pluginId, 'manifest.json');
|
|
59
|
+
const hasManifest = fs_1.default.existsSync(manifestPath);
|
|
60
|
+
let manifest = null;
|
|
61
|
+
if (hasManifest) {
|
|
62
|
+
try {
|
|
63
|
+
manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
}
|
|
67
|
+
const row = [
|
|
68
|
+
chalk_1.default.bold(manifest?.name || pluginId),
|
|
69
|
+
hasManifest ? chalk_1.default.green('Si') : chalk_1.default.dim('No'),
|
|
70
|
+
];
|
|
71
|
+
for (const tenant of tenants) {
|
|
72
|
+
const tp = tenantPlugins.find((r) => r.tenantId === tenant.id && r.pluginId === pluginId);
|
|
73
|
+
if (tp?.isActive) {
|
|
74
|
+
row.push(chalk_1.default.green('Activo'));
|
|
75
|
+
}
|
|
76
|
+
else if (tp) {
|
|
77
|
+
row.push(chalk_1.default.dim('Inactivo'));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
row.push(chalk_1.default.dim('-'));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
table.push(row);
|
|
84
|
+
}
|
|
85
|
+
console.log(table.toString());
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
spinner.fail(err.message);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await (0, db_1.disconnect)();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerSetupCommand = registerSetupCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const bcrypt_1 = __importDefault(require("bcrypt"));
|
|
12
|
+
const db_1 = require("../utils/db");
|
|
13
|
+
const logger_1 = require("../utils/logger");
|
|
14
|
+
function registerSetupCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('setup')
|
|
17
|
+
.description('Configuración inicial de OpenFactu')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk_1.default.bold.white(' OpenFactu — Setup Inicial'));
|
|
21
|
+
console.log(chalk_1.default.dim(' ────────────────────────────────────'));
|
|
22
|
+
console.log();
|
|
23
|
+
try {
|
|
24
|
+
// 1. Verificar conexión
|
|
25
|
+
const connSpinner = (0, ora_1.default)('Verificando conexión a la base de datos...').start();
|
|
26
|
+
const connected = await (0, db_1.testConnection)();
|
|
27
|
+
if (!connected) {
|
|
28
|
+
connSpinner.fail('No se pudo conectar a la base de datos');
|
|
29
|
+
logger_1.log.blank();
|
|
30
|
+
logger_1.log.warn('Verifica que DATABASE_URL esté configurado en .env');
|
|
31
|
+
logger_1.log.warn('Ejemplo: DATABASE_URL=postgresql://openfactu:openfactu_pass@localhost:5432/openfactudb');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
connSpinner.succeed('Conexión a la base de datos establecida');
|
|
35
|
+
const publicDb = (0, db_1.getPublicDb)();
|
|
36
|
+
// 2. Crear tablas del schema público
|
|
37
|
+
const schemaSpinner = (0, ora_1.default)('Verificando schema público...').start();
|
|
38
|
+
try {
|
|
39
|
+
// Verificar si la tabla Tenant existe
|
|
40
|
+
await publicDb.execute(db_1.sql.raw('SELECT 1 FROM "Tenant" LIMIT 1'));
|
|
41
|
+
schemaSpinner.succeed('Schema público OK');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
schemaSpinner.text = 'Creando tablas del schema público...';
|
|
45
|
+
// Las tablas se crean con drizzle push — aquí solo verificamos
|
|
46
|
+
schemaSpinner.warn('Schema público necesita inicialización. Ejecuta: npm run db:push:public');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// 3. Verificar/crear admin
|
|
50
|
+
const adminSpinner = (0, ora_1.default)('Verificando usuario administrador...').start();
|
|
51
|
+
const [existingAdmin] = await publicDb
|
|
52
|
+
.select()
|
|
53
|
+
.from(db_1.schema.globalUsers)
|
|
54
|
+
.where((0, db_1.eq)(db_1.schema.globalUsers.username, 'admin'));
|
|
55
|
+
if (existingAdmin) {
|
|
56
|
+
adminSpinner.succeed('Usuario admin ya existe');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
adminSpinner.text = 'Creando usuario administrador...';
|
|
60
|
+
const { adminPassword } = await inquirer_1.default.prompt([
|
|
61
|
+
{
|
|
62
|
+
type: 'password',
|
|
63
|
+
name: 'adminPassword',
|
|
64
|
+
message: 'Password para el usuario admin:',
|
|
65
|
+
default: 'admin123',
|
|
66
|
+
mask: '*',
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
const hashedPassword = await bcrypt_1.default.hash(adminPassword, 10);
|
|
70
|
+
await publicDb.insert(db_1.schema.globalUsers).values({
|
|
71
|
+
id: crypto_1.default.randomUUID(),
|
|
72
|
+
email: 'admin@openfactu.com',
|
|
73
|
+
username: 'admin',
|
|
74
|
+
password: hashedPassword,
|
|
75
|
+
role: 'SUPERUSER',
|
|
76
|
+
});
|
|
77
|
+
adminSpinner.succeed('Usuario admin creado (admin@openfactu.com)');
|
|
78
|
+
}
|
|
79
|
+
// 4. Verificar/crear primer tenant
|
|
80
|
+
const tenants = await publicDb.select().from(db_1.schema.tenants);
|
|
81
|
+
if (tenants.length > 0) {
|
|
82
|
+
logger_1.log.success(`${tenants.length} tenant(s) ya existen`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
logger_1.log.blank();
|
|
86
|
+
const { createTenant } = await inquirer_1.default.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
name: 'createTenant',
|
|
90
|
+
message: 'No hay empresas. ¿Crear la primera?',
|
|
91
|
+
default: true,
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
if (createTenant) {
|
|
95
|
+
const { tenantName } = await inquirer_1.default.prompt([
|
|
96
|
+
{
|
|
97
|
+
type: 'input',
|
|
98
|
+
name: 'tenantName',
|
|
99
|
+
message: 'Nombre de la empresa:',
|
|
100
|
+
default: 'Mi Empresa',
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
const schemaName = 'tenant_' + tenantName
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
106
|
+
.replace(/^_|_$/g, '');
|
|
107
|
+
const tenantSpinner = (0, ora_1.default)(`Creando empresa "${tenantName}"...`).start();
|
|
108
|
+
const tenantId = crypto_1.default.randomUUID();
|
|
109
|
+
await publicDb.insert(db_1.schema.tenants).values({
|
|
110
|
+
id: tenantId,
|
|
111
|
+
name: tenantName,
|
|
112
|
+
schemaName,
|
|
113
|
+
config: JSON.stringify({ createdAt: new Date(), createdBy: 'CLI Setup' }),
|
|
114
|
+
updatedAt: new Date(),
|
|
115
|
+
});
|
|
116
|
+
await publicDb.execute(db_1.sql.raw(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`));
|
|
117
|
+
tenantSpinner.succeed(`Empresa "${tenantName}" creada (${schemaName})`);
|
|
118
|
+
// Asignar admin al tenant
|
|
119
|
+
const [admin] = await publicDb
|
|
120
|
+
.select()
|
|
121
|
+
.from(db_1.schema.globalUsers)
|
|
122
|
+
.where((0, db_1.eq)(db_1.schema.globalUsers.username, 'admin'));
|
|
123
|
+
if (admin) {
|
|
124
|
+
await publicDb.insert(db_1.schema.userTenantMemberships).values({
|
|
125
|
+
id: crypto_1.default.randomUUID(),
|
|
126
|
+
userId: admin.id,
|
|
127
|
+
tenantId,
|
|
128
|
+
role: 'ADMIN',
|
|
129
|
+
});
|
|
130
|
+
logger_1.log.success('Admin asignado a la empresa');
|
|
131
|
+
}
|
|
132
|
+
logger_1.log.info('Ejecuta "openfactu migrate" para aplicar migraciones al nuevo tenant');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
logger_1.log.blank();
|
|
136
|
+
logger_1.log.success(chalk_1.default.bold('Setup completado'));
|
|
137
|
+
logger_1.log.blank();
|
|
138
|
+
logger_1.log.dim(' Próximos pasos:');
|
|
139
|
+
logger_1.log.dim(' openfactu migrate — Aplicar migraciones');
|
|
140
|
+
logger_1.log.dim(' openfactu tenant list — Ver empresas');
|
|
141
|
+
logger_1.log.dim(' openfactu version — Ver versiones');
|
|
142
|
+
logger_1.log.blank();
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
logger_1.log.error(err.message);
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await (0, db_1.disconnect)();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerTenantCommand = registerTenantCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const db_1 = require("../utils/db");
|
|
13
|
+
const logger_1 = require("../utils/logger");
|
|
14
|
+
function registerTenantCommand(program) {
|
|
15
|
+
const tenant = program
|
|
16
|
+
.command('tenant')
|
|
17
|
+
.description('Gestión de tenants (empresas)');
|
|
18
|
+
// ── openfactu tenant list ──
|
|
19
|
+
tenant
|
|
20
|
+
.command('list')
|
|
21
|
+
.description('Lista todos los tenants')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
const spinner = (0, ora_1.default)('Cargando tenants...').start();
|
|
24
|
+
try {
|
|
25
|
+
const tenants = await (0, db_1.getAllTenants)();
|
|
26
|
+
if (tenants.length === 0) {
|
|
27
|
+
spinner.warn('No hay tenants registrados');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
spinner.succeed(`${tenants.length} tenant(s) encontrado(s)`);
|
|
31
|
+
const table = new cli_table3_1.default({
|
|
32
|
+
head: [chalk_1.default.white('Nombre'), chalk_1.default.white('Schema'), chalk_1.default.white('ID'), chalk_1.default.white('Creado')],
|
|
33
|
+
style: { head: [], border: ['dim'] },
|
|
34
|
+
});
|
|
35
|
+
for (const t of tenants) {
|
|
36
|
+
table.push([
|
|
37
|
+
chalk_1.default.bold(t.name),
|
|
38
|
+
chalk_1.default.cyan(t.schemaName),
|
|
39
|
+
chalk_1.default.dim(t.id.substring(0, 8) + '...'),
|
|
40
|
+
t.createdAt ? new Date(t.createdAt).toLocaleDateString('es-ES') : '-',
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
console.log(table.toString());
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
spinner.fail(err.message);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await (0, db_1.disconnect)();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// ── openfactu tenant create ──
|
|
54
|
+
tenant
|
|
55
|
+
.command('create [name]')
|
|
56
|
+
.description('Crea un nuevo tenant')
|
|
57
|
+
.action(async (name) => {
|
|
58
|
+
try {
|
|
59
|
+
let tenantName = name;
|
|
60
|
+
if (!tenantName) {
|
|
61
|
+
const answers = await inquirer_1.default.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: 'input',
|
|
64
|
+
name: 'name',
|
|
65
|
+
message: 'Nombre de la empresa:',
|
|
66
|
+
validate: (v) => v.trim().length > 0 || 'El nombre es obligatorio',
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
tenantName = answers.name;
|
|
70
|
+
}
|
|
71
|
+
const schemaName = 'tenant_' + tenantName
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
74
|
+
.replace(/^_|_$/g, '');
|
|
75
|
+
logger_1.log.info(`Empresa: ${chalk_1.default.bold(tenantName)}`);
|
|
76
|
+
logger_1.log.info(`Schema: ${chalk_1.default.cyan(schemaName)}`);
|
|
77
|
+
logger_1.log.blank();
|
|
78
|
+
const spinner = (0, ora_1.default)('Creando tenant...').start();
|
|
79
|
+
const publicDb = (0, db_1.getPublicDb)();
|
|
80
|
+
// Verificar que no exista
|
|
81
|
+
const [existing] = await publicDb
|
|
82
|
+
.select()
|
|
83
|
+
.from(db_1.schema.tenants)
|
|
84
|
+
.where((0, db_1.eq)(db_1.schema.tenants.name, tenantName));
|
|
85
|
+
if (existing) {
|
|
86
|
+
spinner.warn(`El tenant "${tenantName}" ya existe (${existing.id})`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Crear registro en la tabla Tenant
|
|
90
|
+
const tenantId = crypto_1.default.randomUUID();
|
|
91
|
+
await publicDb.insert(db_1.schema.tenants).values({
|
|
92
|
+
id: tenantId,
|
|
93
|
+
name: tenantName,
|
|
94
|
+
schemaName,
|
|
95
|
+
config: JSON.stringify({ createdAt: new Date(), createdBy: 'CLI' }),
|
|
96
|
+
updatedAt: new Date(),
|
|
97
|
+
});
|
|
98
|
+
spinner.text = 'Creando schema de PostgreSQL...';
|
|
99
|
+
await publicDb.execute(db_1.sql.raw(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`));
|
|
100
|
+
spinner.text = 'Ejecutando migraciones...';
|
|
101
|
+
// Aplicar migraciones
|
|
102
|
+
const fs = require('fs');
|
|
103
|
+
const path = require('path');
|
|
104
|
+
const MIGRATIONS_DIR = require('../utils/paths').getMigrationsDir();
|
|
105
|
+
if (fs.existsSync(MIGRATIONS_DIR)) {
|
|
106
|
+
const tenantDb = (0, db_1.getTenantDb)(schemaName);
|
|
107
|
+
// Crear tabla de historia
|
|
108
|
+
await tenantDb.execute(db_1.sql.raw(`
|
|
109
|
+
CREATE TABLE IF NOT EXISTS "${schemaName}"."_MigrationHistory" (
|
|
110
|
+
"id" TEXT PRIMARY KEY,
|
|
111
|
+
"description" TEXT,
|
|
112
|
+
"appliedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
113
|
+
)
|
|
114
|
+
`));
|
|
115
|
+
const files = fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort();
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
spinner.text = `Aplicando ${file}...`;
|
|
118
|
+
const migrationId = file.replace('.sql', '');
|
|
119
|
+
const filePath = path.join(MIGRATIONS_DIR, file);
|
|
120
|
+
let rawSql = fs.readFileSync(filePath, 'utf8');
|
|
121
|
+
const processedSql = rawSql.replace(/{{schema}}/g, schemaName);
|
|
122
|
+
const statements = processedSql
|
|
123
|
+
.split(/;(?=(?:[^$]*\$\$[^$]*\$\$)*[^$]*$)/)
|
|
124
|
+
.map((s) => s.trim())
|
|
125
|
+
.filter((s) => s.length > 0);
|
|
126
|
+
for (const statement of statements) {
|
|
127
|
+
await tenantDb.execute(db_1.sql.raw(statement));
|
|
128
|
+
}
|
|
129
|
+
await tenantDb.execute(db_1.sql.raw(`INSERT INTO "${schemaName}"."_MigrationHistory" (id, description) VALUES ('${migrationId}', 'Creado desde CLI')`));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
spinner.succeed('Tenant creado correctamente');
|
|
133
|
+
logger_1.log.blank();
|
|
134
|
+
logger_1.log.success(`ID: ${chalk_1.default.dim(tenantId)}`);
|
|
135
|
+
logger_1.log.success(`Nombre: ${chalk_1.default.bold(tenantName)}`);
|
|
136
|
+
logger_1.log.success(`Schema: ${chalk_1.default.cyan(schemaName)}`);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
logger_1.log.error(err.message);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await (0, db_1.disconnect)();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// ── openfactu tenant sync ──
|
|
147
|
+
tenant
|
|
148
|
+
.command('sync [name]')
|
|
149
|
+
.description('Sincroniza migraciones de un tenant o todos')
|
|
150
|
+
.action(async (name) => {
|
|
151
|
+
const spinner = (0, ora_1.default)('Sincronizando...').start();
|
|
152
|
+
try {
|
|
153
|
+
let tenants;
|
|
154
|
+
if (name) {
|
|
155
|
+
const t = await (0, db_1.getAllTenants)();
|
|
156
|
+
const found = t.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
157
|
+
if (!found) {
|
|
158
|
+
spinner.fail(`Tenant "${name}" no encontrado`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
tenants = [found];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
tenants = await (0, db_1.getAllTenants)();
|
|
165
|
+
}
|
|
166
|
+
spinner.succeed(`Sincronizando ${tenants.length} tenant(s)...`);
|
|
167
|
+
// Usamos el mismo proceso de migración
|
|
168
|
+
const fs = require('fs');
|
|
169
|
+
const path = require('path');
|
|
170
|
+
const MIGRATIONS_DIR = require('../utils/paths').getMigrationsDir();
|
|
171
|
+
const files = fs.existsSync(MIGRATIONS_DIR)
|
|
172
|
+
? fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort()
|
|
173
|
+
: [];
|
|
174
|
+
for (const tenant of tenants) {
|
|
175
|
+
const tenantDb = (0, db_1.getTenantDb)(tenant.schemaName);
|
|
176
|
+
// Asegurar tabla de historia
|
|
177
|
+
await tenantDb.execute(db_1.sql.raw(`
|
|
178
|
+
CREATE TABLE IF NOT EXISTS "${tenant.schemaName}"."_MigrationHistory" (
|
|
179
|
+
"id" TEXT PRIMARY KEY,
|
|
180
|
+
"description" TEXT,
|
|
181
|
+
"appliedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
182
|
+
)
|
|
183
|
+
`));
|
|
184
|
+
const result = await tenantDb.execute(db_1.sql.raw(`SELECT id FROM "${tenant.schemaName}"."_MigrationHistory" ORDER BY id`));
|
|
185
|
+
const applied = result.rows.map((r) => r.id);
|
|
186
|
+
const pending = files.filter((f) => !applied.includes(f.replace('.sql', '')));
|
|
187
|
+
if (pending.length === 0) {
|
|
188
|
+
logger_1.log.dim(` ${tenant.name} — al día`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logger_1.log.info(` ${tenant.name} — ${pending.length} pendiente(s)`);
|
|
192
|
+
for (const file of pending) {
|
|
193
|
+
const migrationId = file.replace('.sql', '');
|
|
194
|
+
const filePath = path.join(MIGRATIONS_DIR, file);
|
|
195
|
+
let rawSql = fs.readFileSync(filePath, 'utf8');
|
|
196
|
+
const processedSql = rawSql.replace(/{{schema}}/g, tenant.schemaName);
|
|
197
|
+
const statements = processedSql
|
|
198
|
+
.split(/;(?=(?:[^$]*\$\$[^$]*\$\$)*[^$]*$)/)
|
|
199
|
+
.map((s) => s.trim())
|
|
200
|
+
.filter((s) => s.length > 0);
|
|
201
|
+
for (const statement of statements) {
|
|
202
|
+
await tenantDb.execute(db_1.sql.raw(statement));
|
|
203
|
+
}
|
|
204
|
+
await tenantDb.execute(db_1.sql.raw(`INSERT INTO "${tenant.schemaName}"."_MigrationHistory" (id, description) VALUES ('${migrationId}', 'Sync CLI')`));
|
|
205
|
+
logger_1.log.success(` ${file}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
logger_1.log.blank();
|
|
210
|
+
logger_1.log.success('Sincronización completada');
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
spinner.fail(err.message);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await (0, db_1.disconnect)();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|