kythia-core 0.9.5-beta → 0.10.1-beta

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.
@@ -4,11 +4,12 @@
4
4
  * @file src/database/KythiaSequelize.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.5-beta
7
+ * @version 0.10.0-beta
8
8
  *
9
9
  * @description
10
10
  * Main Sequelize connection factory for the application.
11
11
  */
12
+
12
13
  const { Sequelize } = require('sequelize');
13
14
 
14
15
  /**
@@ -20,100 +21,108 @@ const { Sequelize } = require('sequelize');
20
21
  * @returns {Sequelize} Configured Sequelize instance
21
22
  */
22
23
  function createSequelizeInstance(config, logger) {
23
- const dbConfig = config.db || {};
24
- if (!config.db) config.db = dbConfig;
25
-
26
- let driver = dbConfig.driver || process.env.DB_DRIVER;
27
- let name = dbConfig.name || process.env.DB_NAME;
28
-
29
- if (!driver || driver === '') {
30
- driver = 'sqlite';
31
- if (logger) logger.info('💡 DB driver not specified. Defaulting to: sqlite');
32
- } else {
33
- driver = driver.toLowerCase();
34
- }
35
-
36
- if (driver === 'sqlite') {
37
- if (!name || name === '') {
38
- name = 'kythiadata.sqlite';
39
- if (logger) logger.info('💡 DB name for sqlite not specified. Defaulting to: kythiadata.sqlite');
40
- }
41
- }
42
-
43
- dbConfig.driver = driver;
44
- dbConfig.name = name;
45
-
46
- const dialect = dbConfig.driver;
47
- const dbName = dbConfig.name;
48
- const dbUser = dbConfig.user || process.env.DB_USER;
49
- const dbPassword = dbConfig.password || process.env.DB_PASSWORD;
50
- const dbHost = dbConfig.host || process.env.DB_HOST;
51
- const dbPort = dbConfig.port || process.env.DB_PORT;
52
-
53
- const dbSocket = dbConfig.socketPath || process.env.DB_SOCKET_PATH;
54
- const dbSsl = dbConfig.ssl || process.env.DB_SSL;
55
- const dbDialectOptions = dbConfig.dialectOptions || process.env.DB_DIALECT_OPTIONS;
56
-
57
- const seqConfig = {
58
- database: dbName,
59
- username: dbUser,
60
- password: dbPassword,
61
- dialect: dialect,
62
- logging: (sql) => {
63
- logger.debug(sql);
64
- },
65
- define: {
66
- charset: 'utf8mb4',
67
- collate: 'utf8mb4_unicode_ci',
68
- },
69
- };
70
-
71
- if (dialect !== 'sqlite') {
72
- seqConfig.timezone = dbConfig.timezone || '+00:00';
73
- }
74
-
75
- switch (dialect) {
76
- case 'sqlite':
77
- seqConfig.storage = dbConfig.name;
78
- delete seqConfig.database;
79
- break;
80
-
81
- case 'mysql':
82
- case 'mariadb':
83
- seqConfig.host = dbHost;
84
- seqConfig.port = dbPort;
85
- if (dbSocket) {
86
- seqConfig.dialectOptions = { socketPath: dbSocket };
87
- }
88
- break;
89
-
90
- case 'postgres':
91
- seqConfig.host = dbHost;
92
- seqConfig.port = dbPort;
93
- if (dbSsl === 'true' || dbSsl === true) {
94
- seqConfig.dialectOptions = {
95
- ssl: { require: true, rejectUnauthorized: false },
96
- };
97
- }
98
- break;
99
-
100
- case 'mssql':
101
- seqConfig.host = dbHost;
102
- seqConfig.port = dbPort;
103
- if (dbDialectOptions) {
104
- try {
105
- seqConfig.dialectOptions = typeof dbDialectOptions === 'string' ? JSON.parse(dbDialectOptions) : dbDialectOptions;
106
- } catch (e) {
107
- logger.error('Error parsing dialect options:', e.message);
108
- }
109
- }
110
- break;
111
-
112
- default:
113
- throw new Error(`${dialect} is not supported or not configured.`);
114
- }
115
-
116
- return new Sequelize(seqConfig);
24
+ const dbConfig = config.db || {};
25
+ if (!config.db) config.db = dbConfig;
26
+
27
+ let driver = dbConfig.driver || process.env.DB_DRIVER;
28
+ let name = dbConfig.name || process.env.DB_NAME;
29
+
30
+ if (!driver || driver === '') {
31
+ driver = 'sqlite';
32
+ if (logger)
33
+ logger.info('💡 DB driver not specified. Defaulting to: sqlite');
34
+ } else {
35
+ driver = driver.toLowerCase();
36
+ }
37
+
38
+ if (driver === 'sqlite') {
39
+ if (!name || name === '') {
40
+ name = 'kythiadata.sqlite';
41
+ if (logger)
42
+ logger.info(
43
+ '💡 DB name for sqlite not specified. Defaulting to: kythiadata.sqlite',
44
+ );
45
+ }
46
+ }
47
+
48
+ dbConfig.driver = driver;
49
+ dbConfig.name = name;
50
+
51
+ const dialect = dbConfig.driver;
52
+ const dbName = dbConfig.name;
53
+ const dbUser = dbConfig.user || process.env.DB_USER;
54
+ const dbPassword = dbConfig.password || process.env.DB_PASSWORD;
55
+ const dbHost = dbConfig.host || process.env.DB_HOST;
56
+ const dbPort = dbConfig.port || process.env.DB_PORT;
57
+
58
+ const dbSocket = dbConfig.socketPath || process.env.DB_SOCKET_PATH;
59
+ const dbSsl = dbConfig.ssl || process.env.DB_SSL;
60
+ const dbDialectOptions =
61
+ dbConfig.dialectOptions || process.env.DB_DIALECT_OPTIONS;
62
+
63
+ const seqConfig = {
64
+ database: dbName,
65
+ username: dbUser,
66
+ password: dbPassword,
67
+ dialect: dialect,
68
+ logging: (sql) => {
69
+ logger.debug(sql);
70
+ },
71
+ define: {
72
+ charset: 'utf8mb4',
73
+ collate: 'utf8mb4_unicode_ci',
74
+ },
75
+ };
76
+
77
+ if (dialect !== 'sqlite') {
78
+ seqConfig.timezone = dbConfig.timezone || '+00:00';
79
+ }
80
+
81
+ switch (dialect) {
82
+ case 'sqlite':
83
+ seqConfig.storage = dbConfig.name;
84
+ delete seqConfig.database;
85
+ break;
86
+
87
+ case 'mysql':
88
+ case 'mariadb':
89
+ seqConfig.host = dbHost;
90
+ seqConfig.port = dbPort;
91
+ if (dbSocket) {
92
+ seqConfig.dialectOptions = { socketPath: dbSocket };
93
+ }
94
+ break;
95
+
96
+ case 'postgres':
97
+ seqConfig.host = dbHost;
98
+ seqConfig.port = dbPort;
99
+ if (dbSsl === 'true' || dbSsl === true) {
100
+ seqConfig.dialectOptions = {
101
+ ssl: { require: true, rejectUnauthorized: false },
102
+ };
103
+ }
104
+ break;
105
+
106
+ case 'mssql':
107
+ seqConfig.host = dbHost;
108
+ seqConfig.port = dbPort;
109
+ if (dbDialectOptions) {
110
+ try {
111
+ seqConfig.dialectOptions =
112
+ typeof dbDialectOptions === 'string'
113
+ ? JSON.parse(dbDialectOptions)
114
+ : dbDialectOptions;
115
+ } catch (e) {
116
+ logger.error('Error parsing dialect options:', e.message);
117
+ }
118
+ }
119
+ break;
120
+
121
+ default:
122
+ throw new Error(`${dialect} is not supported or not configured.`);
123
+ }
124
+
125
+ return new Sequelize(seqConfig);
117
126
  }
118
127
 
119
128
  module.exports = createSequelizeInstance;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * 🗄️ Laravel-Style Migration Storage Adapter
3
+ *
4
+ * @file src/database/KythiaStorage.js
5
+ * @copyright © 2025 kenndeclouv
6
+ * @assistant chaa & graa
7
+ * @version 0.10.0-beta
8
+ *
9
+ * @description
10
+ * Custom storage adapter for Umzug that mimics Laravel's migration table structure.
11
+ * It adds a 'batch' column to the migrations table, allowing for smart rollbacks
12
+ * (undoing only the last batch of migrations instead of just the last file).
13
+ *
14
+ * ✨ Core Features:
15
+ * - Batch Tracking: Stores migration batch numbers.
16
+ * - Auto-Setup: Creates the migrations table automatically if missing.
17
+ * - Smart Rollback Support: Methods to fetch the last batch ID and files.
18
+ */
19
+
20
+ const { DataTypes } = require('sequelize');
21
+
22
+ class KythiaStorage {
23
+ constructor({ sequelize, tableName = 'migrations' }) {
24
+ this.sequelize = sequelize;
25
+ this.tableName = tableName;
26
+ this.currentBatch = 1;
27
+ }
28
+
29
+ async ensureTable() {
30
+ const queryInterface = this.sequelize.getQueryInterface();
31
+
32
+ const tables = await queryInterface.showAllTables();
33
+ if (!tables.includes(this.tableName)) {
34
+ await queryInterface.createTable(this.tableName, {
35
+ id: {
36
+ type: DataTypes.INTEGER,
37
+ primaryKey: true,
38
+ autoIncrement: true,
39
+ },
40
+ name: {
41
+ type: DataTypes.STRING,
42
+ allowNull: false,
43
+ },
44
+ batch: {
45
+ type: DataTypes.INTEGER,
46
+ allowNull: false,
47
+ defaultValue: 1,
48
+ },
49
+ migration_time: {
50
+ type: DataTypes.DATE,
51
+ defaultValue: DataTypes.NOW,
52
+ },
53
+ });
54
+ }
55
+ }
56
+
57
+ async executed() {
58
+ await this.ensureTable();
59
+ const [results] = await this.sequelize.query(
60
+ `SELECT name FROM ${this.tableName} ORDER BY id ASC`,
61
+ );
62
+ return results.map((r) => r.name);
63
+ }
64
+
65
+ async logMigration({ name }) {
66
+ await this.ensureTable();
67
+
68
+ await this.sequelize.query(
69
+ `INSERT INTO ${this.tableName} (name, batch, migration_time) VALUES (?, ?, ?)`,
70
+ {
71
+ replacements: [name, this.currentBatch, new Date()],
72
+ type: this.sequelize.QueryTypes.INSERT,
73
+ },
74
+ );
75
+ }
76
+
77
+ async unlogMigration({ name }) {
78
+ await this.ensureTable();
79
+ await this.sequelize.query(`DELETE FROM ${this.tableName} WHERE name = ?`, {
80
+ replacements: [name],
81
+ type: this.sequelize.QueryTypes.DELETE,
82
+ });
83
+ }
84
+
85
+ setBatch(batchNumber) {
86
+ this.currentBatch = batchNumber;
87
+ }
88
+
89
+ async getLastBatchNumber() {
90
+ await this.ensureTable();
91
+ const [result] = await this.sequelize.query(
92
+ `SELECT MAX(batch) as max_batch FROM ${this.tableName}`,
93
+ {
94
+ type: this.sequelize.QueryTypes.SELECT,
95
+ },
96
+ );
97
+ return result ? result.max_batch || 0 : 0;
98
+ }
99
+
100
+ async getLastBatchMigrations() {
101
+ await this.ensureTable();
102
+ const lastBatch = await this.getLastBatchNumber();
103
+ if (lastBatch === 0) return [];
104
+
105
+ const migrations = await this.sequelize.query(
106
+ `SELECT name FROM ${this.tableName} WHERE batch = ? ORDER BY id DESC`,
107
+ {
108
+ replacements: [lastBatch],
109
+ type: this.sequelize.QueryTypes.SELECT,
110
+ },
111
+ );
112
+
113
+ return migrations.map((m) => m.name);
114
+ }
115
+ }
116
+
117
+ module.exports = KythiaStorage;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 🤖 Automatic Model Loader & Bootstrapper
3
+ *
4
+ * @file src/loaders/ModelLoader.js
5
+ * @copyright © 2025 kenndeclouv
6
+ * @assistant chaa & graa
7
+ * @version 0.10.0-beta
8
+ *
9
+ * @description
10
+ * Scans the `addons` directory for KythiaModel definitions, requires them,
11
+ * and executes their `autoBoot` static method for database introspection.
12
+ * Finally, it links Sequelize associations after all models are loaded.
13
+ *
14
+ * ✨ Core Features:
15
+ * - Addon Scanning: Finds models in `addons/{AddonName}/database/models`.
16
+ * - AutoBoot Execution: Triggers DB introspection (reading columns from DB).
17
+ * - Association Linking: Automatically calls `.associate()` on models if defined.
18
+ * - Container Registration: Registers booted models into the global container.
19
+ */
20
+
21
+ const path = require('node:path');
22
+ const fs = require('node:fs');
23
+
24
+ async function bootModels(kythiaInstance, sequelize) {
25
+ const { container, logger } = kythiaInstance;
26
+ const rootDir = container.appRoot;
27
+ const addonsDir = path.join(rootDir, 'addons');
28
+
29
+ if (!fs.existsSync(addonsDir)) return;
30
+
31
+ const loadedModels = [];
32
+ const addonFolders = fs
33
+ .readdirSync(addonsDir)
34
+ .filter((f) => fs.statSync(path.join(addonsDir, f)).isDirectory());
35
+
36
+ logger.info('📂 Scanning & Booting Models...');
37
+
38
+ for (const addon of addonFolders) {
39
+ const modelsDir = path.join(addonsDir, addon, 'database', 'models');
40
+ if (fs.existsSync(modelsDir)) {
41
+ const files = fs.readdirSync(modelsDir).filter((f) => f.endsWith('.js'));
42
+
43
+ for (const file of files) {
44
+ const modelPath = path.join(modelsDir, file);
45
+ try {
46
+ const ModelClass = require(modelPath);
47
+
48
+ if (ModelClass.autoBoot) {
49
+ loadedModels.push(ModelClass);
50
+ }
51
+ } catch (err) {
52
+ logger.error(`❌ Failed to require model ${file}:`, err);
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ for (const ModelClass of loadedModels) {
59
+ try {
60
+ await ModelClass.autoBoot(sequelize);
61
+
62
+ container.models[ModelClass.name] = ModelClass;
63
+ logger.info(
64
+ ` ✨ Booted: ${ModelClass.name} -> ${ModelClass.tableName}`,
65
+ );
66
+ } catch (err) {
67
+ logger.error(`❌ AutoBoot Failed for ${ModelClass.name}:`, err);
68
+ }
69
+ }
70
+
71
+ logger.info('🔗 Linking Associations...');
72
+ Object.values(container.models).forEach((model) => {
73
+ if (model.associate) {
74
+ model.associate(container.models);
75
+ }
76
+ });
77
+ }
78
+
79
+ module.exports = bootModels;