kythia-core 0.11.0-beta → 0.12.0-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.
Files changed (260) hide show
  1. package/dist/Kythia.d.ts +45 -0
  2. package/dist/Kythia.d.ts.map +1 -0
  3. package/dist/Kythia.js +443 -0
  4. package/dist/Kythia.js.map +1 -0
  5. package/dist/KythiaClient.d.ts +3 -0
  6. package/dist/KythiaClient.d.ts.map +1 -0
  7. package/dist/KythiaClient.js +69 -0
  8. package/dist/KythiaClient.js.map +1 -0
  9. package/dist/cli/Command.d.ts +9 -0
  10. package/dist/cli/Command.d.ts.map +1 -0
  11. package/dist/cli/Command.js +19 -0
  12. package/dist/cli/Command.js.map +1 -0
  13. package/dist/cli/commands/CacheClearCommand.d.ts +8 -0
  14. package/dist/cli/commands/CacheClearCommand.d.ts.map +1 -0
  15. package/dist/cli/commands/CacheClearCommand.js +94 -0
  16. package/dist/cli/commands/CacheClearCommand.js.map +1 -0
  17. package/dist/cli/commands/LangCheckCommand.d.ts +7 -0
  18. package/dist/cli/commands/LangCheckCommand.d.ts.map +1 -0
  19. package/dist/cli/commands/LangCheckCommand.js +345 -0
  20. package/dist/cli/commands/LangCheckCommand.js.map +1 -0
  21. package/dist/cli/commands/LangTranslateCommand.d.ts +8 -0
  22. package/dist/cli/commands/LangTranslateCommand.d.ts.map +1 -0
  23. package/dist/cli/commands/LangTranslateCommand.js +221 -0
  24. package/dist/cli/commands/LangTranslateCommand.js.map +1 -0
  25. package/dist/cli/commands/MakeMigrationCommand.d.ts +7 -0
  26. package/dist/cli/commands/MakeMigrationCommand.d.ts.map +1 -0
  27. package/dist/cli/commands/MakeMigrationCommand.js +55 -0
  28. package/dist/cli/commands/MakeMigrationCommand.js.map +1 -0
  29. package/dist/cli/commands/MakeModelCommand.d.ts +7 -0
  30. package/dist/cli/commands/MakeModelCommand.d.ts.map +1 -0
  31. package/dist/cli/commands/MakeModelCommand.js +56 -0
  32. package/dist/cli/commands/MakeModelCommand.js.map +1 -0
  33. package/dist/cli/commands/MigrateCommand.d.ts +14 -0
  34. package/dist/cli/commands/MigrateCommand.d.ts.map +1 -0
  35. package/dist/cli/commands/MigrateCommand.js +190 -0
  36. package/dist/cli/commands/MigrateCommand.js.map +1 -0
  37. package/dist/cli/commands/NamespaceCommand.d.ts +7 -0
  38. package/dist/cli/commands/NamespaceCommand.d.ts.map +1 -0
  39. package/dist/cli/commands/NamespaceCommand.js +92 -0
  40. package/dist/cli/commands/NamespaceCommand.js.map +1 -0
  41. package/dist/cli/commands/StructureCommand.d.ts +7 -0
  42. package/dist/cli/commands/StructureCommand.d.ts.map +1 -0
  43. package/dist/cli/commands/StructureCommand.js +51 -0
  44. package/dist/cli/commands/StructureCommand.js.map +1 -0
  45. package/dist/cli/commands/UpversionCommand.d.ts +7 -0
  46. package/dist/cli/commands/UpversionCommand.d.ts.map +1 -0
  47. package/dist/cli/commands/UpversionCommand.js +68 -0
  48. package/dist/cli/commands/UpversionCommand.js.map +1 -0
  49. package/dist/cli/index.d.ts +3 -0
  50. package/dist/cli/index.d.ts.map +1 -0
  51. package/dist/cli/index.js +44 -0
  52. package/dist/cli/index.js.map +1 -0
  53. package/dist/cli/utils/db.d.ts +9 -0
  54. package/dist/cli/utils/db.d.ts.map +1 -0
  55. package/dist/cli/utils/db.js +90 -0
  56. package/dist/cli/utils/db.js.map +1 -0
  57. package/dist/database/KythiaMigrator.d.ts +4 -0
  58. package/dist/database/KythiaMigrator.d.ts.map +1 -0
  59. package/dist/database/KythiaMigrator.js +94 -0
  60. package/dist/database/KythiaMigrator.js.map +1 -0
  61. package/dist/database/KythiaModel.d.ts +83 -0
  62. package/dist/database/KythiaModel.d.ts.map +1 -0
  63. package/dist/database/KythiaModel.js +1121 -0
  64. package/dist/database/KythiaModel.js.map +1 -0
  65. package/dist/database/KythiaSequelize.d.ts +4 -0
  66. package/dist/database/KythiaSequelize.d.ts.map +1 -0
  67. package/dist/database/KythiaSequelize.js +99 -0
  68. package/dist/database/KythiaSequelize.js.map +1 -0
  69. package/dist/database/KythiaStorage.d.ts +21 -0
  70. package/dist/database/KythiaStorage.d.ts.map +1 -0
  71. package/dist/database/KythiaStorage.js +80 -0
  72. package/dist/database/KythiaStorage.js.map +1 -0
  73. package/dist/database/ModelLoader.d.ts +4 -0
  74. package/dist/database/ModelLoader.d.ts.map +1 -0
  75. package/dist/database/ModelLoader.js +54 -0
  76. package/dist/database/ModelLoader.js.map +1 -0
  77. package/dist/index.d.ts +10 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +36 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/lang/en.json +85 -0
  82. package/dist/managers/AddonManager.d.ts +45 -0
  83. package/dist/managers/AddonManager.d.ts.map +1 -0
  84. package/dist/managers/AddonManager.js +932 -0
  85. package/dist/managers/AddonManager.js.map +1 -0
  86. package/dist/managers/EventManager.d.ts +19 -0
  87. package/dist/managers/EventManager.d.ts.map +1 -0
  88. package/dist/managers/EventManager.js +55 -0
  89. package/dist/managers/EventManager.js.map +1 -0
  90. package/dist/managers/InteractionManager.d.ts +41 -0
  91. package/dist/managers/InteractionManager.d.ts.map +1 -0
  92. package/dist/managers/InteractionManager.js +441 -0
  93. package/dist/managers/InteractionManager.js.map +1 -0
  94. package/dist/managers/MiddlewareManager.d.ts +14 -0
  95. package/dist/managers/MiddlewareManager.d.ts.map +1 -0
  96. package/dist/managers/MiddlewareManager.js +75 -0
  97. package/dist/managers/MiddlewareManager.js.map +1 -0
  98. package/dist/managers/ShutdownManager.d.ts +22 -0
  99. package/dist/managers/ShutdownManager.d.ts.map +1 -0
  100. package/dist/managers/ShutdownManager.js +151 -0
  101. package/dist/managers/ShutdownManager.js.map +1 -0
  102. package/dist/managers/TranslatorManager.d.ts +19 -0
  103. package/dist/managers/TranslatorManager.d.ts.map +1 -0
  104. package/dist/managers/TranslatorManager.js +118 -0
  105. package/dist/managers/TranslatorManager.js.map +1 -0
  106. package/dist/middlewares/botPermissions.d.ts +4 -0
  107. package/dist/middlewares/botPermissions.d.ts.map +1 -0
  108. package/dist/middlewares/botPermissions.js +28 -0
  109. package/dist/middlewares/botPermissions.js.map +1 -0
  110. package/dist/middlewares/cooldown.d.ts +4 -0
  111. package/dist/middlewares/cooldown.d.ts.map +1 -0
  112. package/dist/middlewares/cooldown.js +42 -0
  113. package/dist/middlewares/cooldown.js.map +1 -0
  114. package/dist/middlewares/isInMainGuild.d.ts +4 -0
  115. package/dist/middlewares/isInMainGuild.d.ts.map +1 -0
  116. package/dist/middlewares/isInMainGuild.js +52 -0
  117. package/dist/middlewares/isInMainGuild.js.map +1 -0
  118. package/dist/middlewares/ownerOnly.d.ts +4 -0
  119. package/dist/middlewares/ownerOnly.d.ts.map +1 -0
  120. package/dist/middlewares/ownerOnly.js +24 -0
  121. package/dist/middlewares/ownerOnly.js.map +1 -0
  122. package/dist/middlewares/teamOnly.d.ts +4 -0
  123. package/dist/middlewares/teamOnly.d.ts.map +1 -0
  124. package/dist/middlewares/teamOnly.js +26 -0
  125. package/dist/middlewares/teamOnly.js.map +1 -0
  126. package/dist/middlewares/userPermissions.d.ts +4 -0
  127. package/dist/middlewares/userPermissions.d.ts.map +1 -0
  128. package/dist/middlewares/userPermissions.js +28 -0
  129. package/dist/middlewares/userPermissions.js.map +1 -0
  130. package/dist/middlewares/voteLocked.d.ts +4 -0
  131. package/dist/middlewares/voteLocked.d.ts.map +1 -0
  132. package/dist/middlewares/voteLocked.js +50 -0
  133. package/dist/middlewares/voteLocked.js.map +1 -0
  134. package/dist/structures/BaseCommand.d.ts +23 -0
  135. package/dist/structures/BaseCommand.d.ts.map +1 -0
  136. package/dist/structures/BaseCommand.js +42 -0
  137. package/dist/structures/BaseCommand.js.map +1 -0
  138. package/dist/types/AddonManager.d.ts +58 -0
  139. package/dist/types/AddonManager.d.ts.map +1 -0
  140. package/dist/types/AddonManager.js +3 -0
  141. package/dist/types/AddonManager.js.map +1 -0
  142. package/dist/types/DiscordHelpers.d.ts +7 -0
  143. package/dist/types/DiscordHelpers.d.ts.map +1 -0
  144. package/dist/types/DiscordHelpers.js +3 -0
  145. package/dist/types/DiscordHelpers.js.map +1 -0
  146. package/dist/types/EventManager.d.ts +10 -0
  147. package/dist/types/EventManager.d.ts.map +1 -0
  148. package/dist/types/EventManager.js +3 -0
  149. package/dist/types/EventManager.js.map +1 -0
  150. package/dist/types/InteractionManager.d.ts +35 -0
  151. package/dist/types/InteractionManager.d.ts.map +1 -0
  152. package/dist/types/InteractionManager.js +3 -0
  153. package/dist/types/InteractionManager.js.map +1 -0
  154. package/dist/types/KythiaClient.d.ts +9 -0
  155. package/dist/types/KythiaClient.d.ts.map +1 -0
  156. package/dist/types/KythiaClient.js +3 -0
  157. package/dist/types/KythiaClient.js.map +1 -0
  158. package/dist/types/KythiaConfig.d.ts +291 -0
  159. package/dist/types/KythiaConfig.d.ts.map +1 -0
  160. package/dist/types/KythiaConfig.js +3 -0
  161. package/dist/types/KythiaConfig.js.map +1 -0
  162. package/dist/types/KythiaContainer.d.ts +38 -0
  163. package/dist/types/KythiaContainer.d.ts.map +1 -0
  164. package/dist/types/KythiaContainer.js +3 -0
  165. package/dist/types/KythiaContainer.js.map +1 -0
  166. package/dist/types/KythiaLogger.d.ts +5 -0
  167. package/dist/types/KythiaLogger.d.ts.map +1 -0
  168. package/dist/types/KythiaLogger.js +3 -0
  169. package/dist/types/KythiaLogger.js.map +1 -0
  170. package/dist/types/KythiaMigrator.d.ts +9 -0
  171. package/dist/types/KythiaMigrator.d.ts.map +1 -0
  172. package/dist/types/KythiaMigrator.js +3 -0
  173. package/dist/types/KythiaMigrator.js.map +1 -0
  174. package/dist/types/KythiaModel.d.ts +31 -0
  175. package/dist/types/KythiaModel.d.ts.map +1 -0
  176. package/dist/types/KythiaModel.js +3 -0
  177. package/dist/types/KythiaModel.js.map +1 -0
  178. package/dist/types/KythiaOptions.d.ts +13 -0
  179. package/dist/types/KythiaOptions.d.ts.map +1 -0
  180. package/dist/types/KythiaOptions.js +3 -0
  181. package/dist/types/KythiaOptions.js.map +1 -0
  182. package/dist/types/KythiaSequelize.d.ts +13 -0
  183. package/dist/types/KythiaSequelize.d.ts.map +1 -0
  184. package/dist/types/KythiaSequelize.js +3 -0
  185. package/dist/types/KythiaSequelize.js.map +1 -0
  186. package/dist/types/KythiaStorage.d.ts +22 -0
  187. package/dist/types/KythiaStorage.d.ts.map +1 -0
  188. package/dist/types/KythiaStorage.js +3 -0
  189. package/dist/types/KythiaStorage.js.map +1 -0
  190. package/dist/types/MiddlewareManager.d.ts +14 -0
  191. package/dist/types/MiddlewareManager.d.ts.map +1 -0
  192. package/dist/types/MiddlewareManager.js +3 -0
  193. package/dist/types/MiddlewareManager.js.map +1 -0
  194. package/dist/types/ModelLoader.d.ts +8 -0
  195. package/dist/types/ModelLoader.d.ts.map +1 -0
  196. package/dist/types/ModelLoader.js +3 -0
  197. package/dist/types/ModelLoader.js.map +1 -0
  198. package/dist/types/ShutdownManager.d.ts +15 -0
  199. package/dist/types/ShutdownManager.d.ts.map +1 -0
  200. package/dist/types/ShutdownManager.js +3 -0
  201. package/dist/types/ShutdownManager.js.map +1 -0
  202. package/dist/types/TranslatorManager.d.ts +16 -0
  203. package/dist/types/TranslatorManager.d.ts.map +1 -0
  204. package/dist/types/TranslatorManager.js +3 -0
  205. package/dist/types/TranslatorManager.js.map +1 -0
  206. package/dist/types/index.d.ts +13 -0
  207. package/dist/types/index.d.ts.map +1 -0
  208. package/dist/types/index.js +29 -0
  209. package/dist/types/index.js.map +1 -0
  210. package/dist/utils/color.d.ts +15 -0
  211. package/dist/utils/color.d.ts.map +1 -0
  212. package/dist/utils/color.js +156 -0
  213. package/dist/utils/color.js.map +1 -0
  214. package/dist/utils/discord.d.ts +8 -0
  215. package/dist/utils/discord.d.ts.map +1 -0
  216. package/dist/utils/discord.js +53 -0
  217. package/dist/utils/discord.js.map +1 -0
  218. package/dist/utils/formatter.d.ts +3 -0
  219. package/dist/utils/formatter.d.ts.map +1 -0
  220. package/dist/utils/formatter.js +89 -0
  221. package/dist/utils/formatter.js.map +1 -0
  222. package/dist/utils/index.d.ts +12 -0
  223. package/dist/utils/index.d.ts.map +1 -0
  224. package/dist/utils/index.js +54 -0
  225. package/dist/utils/index.js.map +1 -0
  226. package/dist/utils/logger.d.ts +5 -0
  227. package/dist/utils/logger.d.ts.map +1 -0
  228. package/dist/utils/logger.js +150 -0
  229. package/dist/utils/logger.js.map +1 -0
  230. package/package.json +28 -6
  231. package/src/lang/en.json +85 -0
  232. package/changelog.md +0 -53
  233. package/index.js +0 -15
  234. package/src/Kythia.js +0 -556
  235. package/src/KythiaClient.js +0 -94
  236. package/src/cli/Command.js +0 -68
  237. package/src/cli/commands/CacheClearCommand.js +0 -136
  238. package/src/cli/commands/LangCheckCommand.js +0 -367
  239. package/src/cli/commands/LangTranslateCommand.js +0 -336
  240. package/src/cli/commands/MakeMigrationCommand.js +0 -82
  241. package/src/cli/commands/MakeModelCommand.js +0 -81
  242. package/src/cli/commands/MigrateCommand.js +0 -259
  243. package/src/cli/commands/NamespaceCommand.js +0 -112
  244. package/src/cli/commands/StructureCommand.js +0 -70
  245. package/src/cli/commands/UpversionCommand.js +0 -94
  246. package/src/cli/index.js +0 -69
  247. package/src/cli/utils/db.js +0 -117
  248. package/src/database/KythiaMigrator.js +0 -116
  249. package/src/database/KythiaModel.js +0 -1557
  250. package/src/database/KythiaSequelize.js +0 -128
  251. package/src/database/KythiaStorage.js +0 -117
  252. package/src/database/ModelLoader.js +0 -79
  253. package/src/managers/AddonManager.js +0 -1219
  254. package/src/managers/EventManager.js +0 -104
  255. package/src/managers/InteractionManager.js +0 -815
  256. package/src/managers/ShutdownManager.js +0 -218
  257. package/src/structures/BaseCommand.js +0 -53
  258. package/src/utils/color.js +0 -180
  259. package/src/utils/formatter.js +0 -99
  260. package/src/utils/index.js +0 -4
@@ -0,0 +1,1121 @@
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.KythiaModel = void 0;
7
+ const jsonStringify = require("json-stable-stringify");
8
+ const sequelize_1 = require("sequelize");
9
+ const lru_cache_1 = require("lru-cache");
10
+ const logger_1 = __importDefault(require("../utils/logger"));
11
+ const NEGATIVE_CACHE_PLACEHOLDER = '__KYTHIA_NEGATIVE_CACHE__';
12
+ const RECONNECT_DELAY_MINUTES = 3;
13
+ const REDIS_ERROR_TOLERANCE_COUNT = 3;
14
+ const REDIS_ERROR_TOLERANCE_INTERVAL_MS = 10 * 1000;
15
+ function safeStringify(obj, logger) {
16
+ try {
17
+ return JSON.stringify(obj, (_key, value) => typeof value === 'bigint' ? value.toString() : value);
18
+ }
19
+ catch (err) {
20
+ (logger || console).error(`❌ [SAFE STRINGIFY] Failed: ${err.message}`);
21
+ return '{}';
22
+ }
23
+ }
24
+ function safeParse(str, logger) {
25
+ try {
26
+ return JSON.parse(str);
27
+ }
28
+ catch {
29
+ (logger || console).warn('⚠️ [SAFE PARSE] Invalid JSON data, returning null');
30
+ return null;
31
+ }
32
+ }
33
+ class KythiaModel extends sequelize_1.Model {
34
+ static client;
35
+ static redis;
36
+ static isRedisConnected = false;
37
+ static logger = logger_1.default;
38
+ static config = {};
39
+ static CACHE_VERSION = '1.0.0';
40
+ static localCache = new lru_cache_1.LRUCache({
41
+ max: 1000,
42
+ });
43
+ static localNegativeCache = new Set();
44
+ static MAX_LOCAL_CACHE_SIZE = 1000;
45
+ static DEFAULT_TTL = 60 * 60 * 1000;
46
+ static lastRedisOpts = null;
47
+ static reconnectTimeout = null;
48
+ static lastAutoReconnectTs = 0;
49
+ static pendingQueries = new Map();
50
+ static cacheStats = {
51
+ redisHits: 0,
52
+ mapHits: 0,
53
+ misses: 0,
54
+ sets: 0,
55
+ clears: 0,
56
+ errors: 0,
57
+ };
58
+ static redisErrorTimestamps = [];
59
+ static isShardMode = false;
60
+ static _redisFallbackURLs = [];
61
+ static _redisCurrentIndex = 0;
62
+ static _redisFailedIndexes = new Set();
63
+ static _justFailedOver = false;
64
+ static fillable;
65
+ static guarded;
66
+ static structure;
67
+ static table;
68
+ static cacheKeys;
69
+ static CACHE_KEYS;
70
+ static CACHE_TTL;
71
+ static customInvalidationTags;
72
+ static init(attributes, options) {
73
+ const model = super.init(attributes, options);
74
+ model.addHook('beforeValidate', (instance) => {
75
+ const ModelClass = instance.constructor;
76
+ if (ModelClass.fillable && Array.isArray(ModelClass.fillable)) {
77
+ const allowedFields = ModelClass.fillable;
78
+ Object.keys(instance.dataValues).forEach((key) => {
79
+ if (!allowedFields.includes(key)) {
80
+ delete instance.dataValues[key];
81
+ if (instance.changed())
82
+ instance.changed(key, false);
83
+ }
84
+ });
85
+ }
86
+ else if (ModelClass.guarded && Array.isArray(ModelClass.guarded)) {
87
+ const forbiddenFields = ModelClass.guarded;
88
+ if (forbiddenFields.includes('*')) {
89
+ instance.dataValues = {};
90
+ return;
91
+ }
92
+ Object.keys(instance.dataValues).forEach((key) => {
93
+ if (forbiddenFields.includes(key)) {
94
+ delete instance.dataValues[key];
95
+ if (instance.changed())
96
+ instance.changed(key, false);
97
+ }
98
+ });
99
+ }
100
+ });
101
+ return model;
102
+ }
103
+ static async autoBoot(sequelize) {
104
+ let tableName = this.table;
105
+ if (!tableName) {
106
+ const modelName = this.name;
107
+ const snakeCase = sequelize_1.Utils.underscoredIf(modelName, true);
108
+ tableName = sequelize_1.Utils.pluralize(snakeCase);
109
+ }
110
+ let manualAttributes = {};
111
+ let manualOptions = {};
112
+ if (this.structure) {
113
+ manualAttributes = this.structure.attributes || {};
114
+ manualOptions = this.structure.options || {};
115
+ }
116
+ const queryInterface = sequelize.getQueryInterface();
117
+ let tableSchema;
118
+ try {
119
+ tableSchema = await queryInterface.describeTable(tableName);
120
+ }
121
+ catch (error) {
122
+ console.warn(`⚠️ [KythiaModel] Table '${tableName}' not found for model '${this.name}'. Skipping auto-boot. err ${error}`);
123
+ return;
124
+ }
125
+ const dbAttributes = {};
126
+ for (const [colName, colInfo] of Object.entries(tableSchema)) {
127
+ dbAttributes[colName] = {
128
+ type: this._mapDbTypeToSequelize(colInfo.type),
129
+ allowNull: colInfo.allowNull,
130
+ defaultValue: colInfo.defaultValue,
131
+ primaryKey: colInfo.primaryKey,
132
+ autoIncrement: colInfo.autoIncrement,
133
+ };
134
+ }
135
+ const finalAttributes = { ...dbAttributes, ...manualAttributes };
136
+ super.init(finalAttributes, {
137
+ sequelize,
138
+ modelName: this.name,
139
+ tableName: tableName,
140
+ timestamps: manualOptions.timestamps !== undefined
141
+ ? manualOptions.timestamps
142
+ : !!finalAttributes.createdAt,
143
+ paranoid: manualOptions.paranoid !== undefined
144
+ ? manualOptions.paranoid
145
+ : !!finalAttributes.deletedAt,
146
+ ...manualOptions,
147
+ });
148
+ this._setupLaravelHooks();
149
+ return this;
150
+ }
151
+ static _mapDbTypeToSequelize(dbType) {
152
+ const type = dbType.toUpperCase();
153
+ if (type.startsWith('BOOLEAN') || type.startsWith('TINYINT(1)'))
154
+ return sequelize_1.DataTypes.BOOLEAN;
155
+ if (type.startsWith('INT') ||
156
+ type.startsWith('TINYINT') ||
157
+ type.startsWith('BIGINT'))
158
+ return sequelize_1.DataTypes.INTEGER;
159
+ if (type.startsWith('VARCHAR') ||
160
+ type.startsWith('TEXT') ||
161
+ type.startsWith('CHAR'))
162
+ return sequelize_1.DataTypes.STRING;
163
+ if (type.startsWith('DATETIME') || type.startsWith('TIMESTAMP'))
164
+ return sequelize_1.DataTypes.DATE;
165
+ if (type.startsWith('JSON'))
166
+ return sequelize_1.DataTypes.JSON;
167
+ if (type.startsWith('FLOAT') ||
168
+ type.startsWith('DOUBLE') ||
169
+ type.startsWith('DECIMAL'))
170
+ return sequelize_1.DataTypes.FLOAT;
171
+ if (type.startsWith('ENUM'))
172
+ return sequelize_1.DataTypes.STRING;
173
+ return sequelize_1.DataTypes.STRING;
174
+ }
175
+ static _setupLaravelHooks() {
176
+ this.addHook('beforeValidate', (instance) => {
177
+ const ModelClass = instance.constructor;
178
+ const pkAttribute = ModelClass.primaryKeyAttribute || 'id';
179
+ if (ModelClass.fillable && Array.isArray(ModelClass.fillable)) {
180
+ const allowedFields = ModelClass.fillable;
181
+ Object.keys(instance.dataValues).forEach((key) => {
182
+ if (key === pkAttribute)
183
+ return;
184
+ if (!allowedFields.includes(key)) {
185
+ delete instance.dataValues[key];
186
+ if (instance.changed())
187
+ instance.changed(key, false);
188
+ }
189
+ });
190
+ }
191
+ else if (ModelClass.guarded && Array.isArray(ModelClass.guarded)) {
192
+ const forbiddenFields = ModelClass.guarded;
193
+ if (forbiddenFields.includes('*')) {
194
+ instance.dataValues = {};
195
+ return;
196
+ }
197
+ Object.keys(instance.dataValues).forEach((key) => {
198
+ if (key === pkAttribute)
199
+ return;
200
+ if (forbiddenFields.includes(key)) {
201
+ delete instance.dataValues[key];
202
+ if (instance.changed())
203
+ instance.changed(key, false);
204
+ }
205
+ });
206
+ }
207
+ });
208
+ }
209
+ static setDependencies({ logger, config, redis, redisOptions, }) {
210
+ if (!config) {
211
+ throw new Error('KythiaModel.setDependencies requires config!');
212
+ }
213
+ this.logger = logger || logger_1.default;
214
+ this.config = config;
215
+ this.CACHE_VERSION = config.db?.redisCacheVersion || '1.0.0';
216
+ const redisConfig = config?.db?.redis;
217
+ this.isShardMode =
218
+ (typeof redisConfig === 'object' &&
219
+ redisConfig !== null &&
220
+ redisConfig.shard) ||
221
+ false;
222
+ if (this.isShardMode) {
223
+ this.logger.info('🟣 [REDIS][SHARD] Detected redis sharding mode (shard: true). Local fallback cache DISABLED!');
224
+ }
225
+ this._redisFallbackURLs = [];
226
+ if (Array.isArray(redisOptions)) {
227
+ this._redisFallbackURLs = redisOptions.filter((opt) => opt && (typeof opt !== 'string' || opt.trim().length > 0));
228
+ }
229
+ else if (typeof redisOptions === 'string') {
230
+ if (redisOptions.trim().length > 0) {
231
+ this._redisFallbackURLs = redisOptions
232
+ .split(',')
233
+ .map((url) => url.trim())
234
+ .filter((url) => url.length > 0);
235
+ }
236
+ }
237
+ else if (redisOptions &&
238
+ typeof redisOptions === 'object' &&
239
+ Array.isArray(redisOptions.urls)) {
240
+ this._redisFallbackURLs = redisOptions.urls.slice();
241
+ }
242
+ else if (redisOptions &&
243
+ typeof redisOptions === 'object' &&
244
+ Object.keys(redisOptions).length > 0) {
245
+ this._redisFallbackURLs = [redisOptions];
246
+ }
247
+ this._redisCurrentIndex = 0;
248
+ if (redis) {
249
+ this.redis = redis;
250
+ this.isRedisConnected = redis.status === 'ready';
251
+ }
252
+ else if (this._redisFallbackURLs.length > 0) {
253
+ this.initializeRedis();
254
+ }
255
+ else {
256
+ if (this.isShardMode) {
257
+ this.logger.error('❌ [REDIS][SHARD] No Redis client/options, but shard:true. Application will work WITHOUT caching!');
258
+ this.isRedisConnected = false;
259
+ }
260
+ else {
261
+ this.logger.warn('🟠 [REDIS] No Redis provided. Switching to In-Memory Cache mode.');
262
+ this.isRedisConnected = false;
263
+ }
264
+ }
265
+ }
266
+ static _trackRedisError(err) {
267
+ const now = Date.now();
268
+ this.redisErrorTimestamps = (this.redisErrorTimestamps || []).filter((ts) => now - ts < REDIS_ERROR_TOLERANCE_INTERVAL_MS);
269
+ this.redisErrorTimestamps.push(now);
270
+ if (this.redisErrorTimestamps.length >= REDIS_ERROR_TOLERANCE_COUNT) {
271
+ if (this.isRedisConnected) {
272
+ const triedFallback = this._tryRedisFailover();
273
+ if (triedFallback) {
274
+ this.logger.warn(`[REDIS] Error tolerance reached, switching to NEXT Redis failover...`);
275
+ }
276
+ else if (this.isShardMode) {
277
+ this.logger.error(`❌ [REDIS][SHARD] ${this.redisErrorTimestamps.length} consecutive errors in ${REDIS_ERROR_TOLERANCE_INTERVAL_MS / 1000}s. SHARD MODE: Disabling cache (NO fallback), all queries go to DB. (Last error: ${err?.message})`);
278
+ this.isRedisConnected = false;
279
+ this._scheduleReconnect();
280
+ }
281
+ else {
282
+ this.logger.error(`❌ [REDIS] ${this.redisErrorTimestamps.length} consecutive errors in ${REDIS_ERROR_TOLERANCE_INTERVAL_MS / 1000}s. All Redis exhausted, fallback to In-Memory Cache! (Last error: ${err?.message})`);
283
+ this.isRedisConnected = false;
284
+ this._scheduleReconnect();
285
+ }
286
+ }
287
+ this.redisErrorTimestamps = [];
288
+ }
289
+ else {
290
+ this.logger.warn(`🟠 [REDIS] Error #${this.redisErrorTimestamps.length}/${REDIS_ERROR_TOLERANCE_COUNT} tolerated. (${err?.message})`);
291
+ }
292
+ }
293
+ static _tryRedisFailover() {
294
+ if (!Array.isArray(this._redisFallbackURLs) ||
295
+ this._redisFallbackURLs.length < 2) {
296
+ return false;
297
+ }
298
+ const prevIndex = this._redisCurrentIndex;
299
+ if (this._redisCurrentIndex + 1 < this._redisFallbackURLs.length) {
300
+ this._redisCurrentIndex++;
301
+ this.logger.warn(`[REDIS][FAILOVER] Trying to switch Redis connection from url index ${prevIndex} to ${this._redisCurrentIndex}`);
302
+ this._justFailedOver = true;
303
+ this._closeCurrentRedis();
304
+ this.initializeRedis();
305
+ return true;
306
+ }
307
+ return false;
308
+ }
309
+ static _closeCurrentRedis() {
310
+ if (this.redis && typeof this.redis.quit === 'function') {
311
+ try {
312
+ this.redis.quit();
313
+ }
314
+ catch (e) {
315
+ console.log(e);
316
+ }
317
+ }
318
+ this.redis = undefined;
319
+ this.isRedisConnected = false;
320
+ }
321
+ static initializeRedis(redisOptions) {
322
+ if (redisOptions) {
323
+ if (Array.isArray(redisOptions)) {
324
+ this._redisFallbackURLs = redisOptions.slice();
325
+ this._redisCurrentIndex = 0;
326
+ }
327
+ else if (redisOptions &&
328
+ typeof redisOptions === 'object' &&
329
+ Array.isArray(redisOptions.urls)) {
330
+ this._redisFallbackURLs = redisOptions.urls.slice();
331
+ this._redisCurrentIndex = 0;
332
+ }
333
+ else {
334
+ this._redisFallbackURLs = [redisOptions];
335
+ this._redisCurrentIndex = 0;
336
+ }
337
+ }
338
+ if (!Array.isArray(this._redisFallbackURLs) ||
339
+ this._redisFallbackURLs.length === 0) {
340
+ if (this.isShardMode) {
341
+ this.logger.error('❌ [REDIS][SHARD] No Redis URL/options provided but shard:true. Will run without caching!');
342
+ this.isRedisConnected = false;
343
+ }
344
+ else {
345
+ this.logger.warn('🟠 [REDIS] No Redis client or options provided. Operating in In-Memory Cache mode only.');
346
+ this.isRedisConnected = false;
347
+ }
348
+ return null;
349
+ }
350
+ const Redis = require('ioredis');
351
+ this.lastRedisOpts = Array.isArray(this._redisFallbackURLs)
352
+ ? this._redisFallbackURLs.slice()
353
+ : [this._redisFallbackURLs];
354
+ if (this.redis)
355
+ return this.redis;
356
+ const opt = this._redisFallbackURLs[this._redisCurrentIndex];
357
+ if (opt && typeof opt === 'object' && opt.shard) {
358
+ this.isShardMode = true;
359
+ }
360
+ let redisOpt;
361
+ if (typeof opt === 'string') {
362
+ redisOpt = { url: opt, retryStrategy: this._makeRetryStrategy() };
363
+ }
364
+ else if (opt && typeof opt === 'object') {
365
+ redisOpt = {
366
+ maxRetriesPerRequest: 2,
367
+ enableReadyCheck: true,
368
+ retryStrategy: this._makeRetryStrategy(),
369
+ ...opt,
370
+ };
371
+ }
372
+ else {
373
+ this.logger.error('❌ [REDIS] Invalid redis config detected in list');
374
+ this.isRedisConnected = false;
375
+ return null;
376
+ }
377
+ this.logger.info(`[REDIS][INIT] Connecting to Redis fallback #${this._redisCurrentIndex + 1}/${this._redisFallbackURLs.length}: ${typeof opt === 'string' ? opt : redisOpt.url || '(object)'}`);
378
+ this.redis = new Redis(redisOpt.url || redisOpt);
379
+ this._setupRedisEventHandlers();
380
+ return this.redis;
381
+ }
382
+ static _makeRetryStrategy() {
383
+ return (times) => {
384
+ if (times > 5) {
385
+ this.logger.error(`❌ [REDIS] Could not connect after ${times - 1} retries for Redis #${this._redisCurrentIndex + 1}.`);
386
+ return null;
387
+ }
388
+ const delay = Math.min(times * 500, 2000);
389
+ this.logger.warn(`🟠 [REDIS] Connection failed for Redis #${this._redisCurrentIndex + 1}. Retrying in ${delay}ms (Attempt ${times})...`);
390
+ return delay;
391
+ };
392
+ }
393
+ static _setupRedisEventHandlers() {
394
+ if (!this.redis)
395
+ return;
396
+ this.redis.on('connect', async () => {
397
+ if (!this.isRedisConnected) {
398
+ this.logger.info('✅ [REDIS] Connection established. Switching to Redis Cache mode.');
399
+ }
400
+ this.isRedisConnected = true;
401
+ this.redisErrorTimestamps = [];
402
+ if (this.reconnectTimeout) {
403
+ clearTimeout(this.reconnectTimeout);
404
+ this.reconnectTimeout = null;
405
+ }
406
+ this._redisFailedIndexes.delete(this._redisCurrentIndex);
407
+ if (this._justFailedOver) {
408
+ this.logger.warn(`[REDIS][FAILOVER] Connected to new server, flushing potentially stale cache...`);
409
+ try {
410
+ await this.redis.flushdb();
411
+ this.logger.info(`[REDIS][FAILOVER] Stale cache flushed successfully.`);
412
+ }
413
+ catch (err) {
414
+ this.logger.error(`[REDIS][FAILOVER] FAILED TO FLUSH CACHE:`, err);
415
+ }
416
+ this._justFailedOver = false;
417
+ }
418
+ });
419
+ this.redis.on('error', (err) => {
420
+ if (err && (err.code === 'ECONNREFUSED' || err.message)) {
421
+ this.logger.warn(`🟠 [REDIS] Connection error: ${err.message}`);
422
+ }
423
+ });
424
+ this.redis.on('close', () => {
425
+ if (this.isRedisConnected) {
426
+ if (this.isShardMode) {
427
+ this.logger.error('❌ [REDIS][SHARD] Connection closed. Cache DISABLED (no fallback).');
428
+ }
429
+ else {
430
+ this.logger.error('❌ [REDIS] Connection closed. Fallback/failover will be attempted.');
431
+ }
432
+ }
433
+ this.isRedisConnected = false;
434
+ this._redisFailedIndexes.add(this._redisCurrentIndex);
435
+ this.logger.warn(`[REDIS] Connection #${this._redisCurrentIndex + 1} closed. Attempting immediate failover...`);
436
+ const triedFailover = this._tryRedisFailover();
437
+ if (!triedFailover) {
438
+ this.logger.warn(`[REDIS] Failover exhausted. Scheduling full reconnect...`);
439
+ this._scheduleReconnect();
440
+ }
441
+ });
442
+ }
443
+ static _scheduleReconnect() {
444
+ if (this.reconnectTimeout)
445
+ return;
446
+ const sinceLast = Date.now() - this.lastAutoReconnectTs;
447
+ if (sinceLast < RECONNECT_DELAY_MINUTES * 60 * 1000)
448
+ return;
449
+ this.lastAutoReconnectTs = Date.now();
450
+ if (this.isShardMode) {
451
+ this.logger.warn(`[REDIS][SHARD] Attempting auto-reconnect after ${RECONNECT_DELAY_MINUTES}min downtime...`);
452
+ }
453
+ else {
454
+ this.logger.warn(`🟢 [REDIS] Attempting auto-reconnect after ${RECONNECT_DELAY_MINUTES}min downtime...`);
455
+ }
456
+ this.reconnectTimeout = setTimeout(() => {
457
+ this.reconnectTimeout = null;
458
+ this._redisCurrentIndex = 0;
459
+ this._redisFailedIndexes.clear();
460
+ this._closeCurrentRedis();
461
+ this.initializeRedis();
462
+ }, RECONNECT_DELAY_MINUTES * 60 * 1000);
463
+ }
464
+ static getCacheKey(queryIdentifier) {
465
+ let dataToHash = queryIdentifier;
466
+ if (dataToHash &&
467
+ typeof dataToHash === 'object' &&
468
+ !dataToHash.where &&
469
+ !dataToHash.include) {
470
+ dataToHash = { where: dataToHash };
471
+ }
472
+ const opts = {
473
+ replacer: (_key, value) => typeof value === 'bigint' ? value.toString() : value,
474
+ };
475
+ const keyBody = typeof queryIdentifier === 'string'
476
+ ? queryIdentifier
477
+ : jsonStringify(this.normalizeQueryOptions(dataToHash), opts);
478
+ return `${this.CACHE_VERSION}:${this.name}:${keyBody}`;
479
+ }
480
+ static normalizeQueryOptions(data) {
481
+ if (!data || typeof data !== 'object')
482
+ return data;
483
+ if (Array.isArray(data))
484
+ return data.map((item) => this.normalizeQueryOptions(item));
485
+ const normalized = {};
486
+ Object.keys(data)
487
+ .sort()
488
+ .forEach((key) => {
489
+ normalized[key] = this.normalizeQueryOptions(data[key]);
490
+ });
491
+ Object.getOwnPropertySymbols(data).forEach((symbol) => {
492
+ const key = `$${symbol.toString().slice(7, -1)}`;
493
+ normalized[key] = this.normalizeQueryOptions(data[symbol]);
494
+ });
495
+ return normalized;
496
+ }
497
+ static _generateSmartTags(instance) {
498
+ if (!instance)
499
+ return [`${this.name}`];
500
+ const tags = [`${this.name}`];
501
+ const pk = this.primaryKeyAttribute;
502
+ if (instance[pk]) {
503
+ tags.push(`${this.name}:${pk}:${instance[pk]}`);
504
+ }
505
+ const smartKeys = this.cacheKeys || this.CACHE_KEYS || [];
506
+ if (Array.isArray(smartKeys)) {
507
+ for (const keyGroup of smartKeys) {
508
+ const keys = Array.isArray(keyGroup) ? keyGroup : [keyGroup];
509
+ const hasAllValues = keys.every((k) => instance[k] !== undefined && instance[k] !== null);
510
+ if (hasAllValues) {
511
+ const tagParts = keys
512
+ .map((k) => `${k}:${instance[k]}`)
513
+ .join(':');
514
+ tags.push(`${this.name}:${tagParts}`);
515
+ }
516
+ }
517
+ }
518
+ return tags;
519
+ }
520
+ static async setCacheEntry(cacheKeyOrQuery, data, ttl, tags = []) {
521
+ const cacheKey = typeof cacheKeyOrQuery === 'string'
522
+ ? cacheKeyOrQuery
523
+ : this.getCacheKey(cacheKeyOrQuery);
524
+ const finalTtl = ttl || this.CACHE_TTL || this.DEFAULT_TTL;
525
+ if (this.isRedisConnected) {
526
+ await this._redisSetCacheEntry(cacheKey, data, finalTtl, tags);
527
+ }
528
+ else if (!this.isShardMode) {
529
+ this._mapSetCacheEntry(cacheKey, data, finalTtl);
530
+ }
531
+ }
532
+ static async getCachedEntry(cacheKeyOrQuery, includeOptions) {
533
+ const cacheKey = typeof cacheKeyOrQuery === 'string'
534
+ ? cacheKeyOrQuery
535
+ : this.getCacheKey(cacheKeyOrQuery);
536
+ if (this.isRedisConnected) {
537
+ return this._redisGetCachedEntry(cacheKey, includeOptions);
538
+ }
539
+ else if (!this.isShardMode) {
540
+ return this._mapGetCachedEntry(cacheKey, includeOptions);
541
+ }
542
+ return { hit: false, data: undefined };
543
+ }
544
+ static async clearCache(keys) {
545
+ const cacheKey = typeof keys === 'string' ? keys : this.getCacheKey(keys);
546
+ if (this.isRedisConnected) {
547
+ await this._redisClearCache(cacheKey);
548
+ }
549
+ else if (!this.isShardMode) {
550
+ this._mapClearCache(cacheKey);
551
+ }
552
+ }
553
+ static async _redisSetCacheEntry(cacheKey, data, ttl, tags = []) {
554
+ if (!this.redis)
555
+ return;
556
+ try {
557
+ let plainData = data;
558
+ if (data && typeof data.toJSON === 'function') {
559
+ plainData = data.toJSON();
560
+ }
561
+ else if (Array.isArray(data)) {
562
+ plainData = data.map((item) => item && typeof item.toJSON === 'function' ? item.toJSON() : item);
563
+ }
564
+ const valueToStore = plainData === null
565
+ ? NEGATIVE_CACHE_PLACEHOLDER
566
+ : safeStringify(plainData, this.logger);
567
+ const multi = this.redis.multi();
568
+ multi.set(cacheKey, valueToStore, 'PX', ttl);
569
+ for (const tag of tags) {
570
+ multi.sadd(tag, cacheKey);
571
+ }
572
+ await multi.exec();
573
+ this.cacheStats.sets++;
574
+ }
575
+ catch (err) {
576
+ this._trackRedisError(err);
577
+ }
578
+ }
579
+ static async _redisGetCachedEntry(cacheKey, includeOptions) {
580
+ if (!this.redis)
581
+ return { hit: false, data: undefined };
582
+ try {
583
+ const result = await this.redis.get(cacheKey);
584
+ if (result === null || result === undefined)
585
+ return { hit: false, data: undefined };
586
+ this.cacheStats.redisHits++;
587
+ if (result === NEGATIVE_CACHE_PLACEHOLDER) {
588
+ return { hit: true, data: null };
589
+ }
590
+ const parsedData = safeParse(result, this.logger);
591
+ if (parsedData === null) {
592
+ return { hit: false, data: undefined };
593
+ }
594
+ const includeAsArray = includeOptions
595
+ ? Array.isArray(includeOptions)
596
+ ? includeOptions
597
+ : [includeOptions]
598
+ : undefined;
599
+ const buildInstance = (data) => {
600
+ const instance = this.build(data, {
601
+ isNewRecord: false,
602
+ include: includeAsArray,
603
+ });
604
+ return instance;
605
+ };
606
+ if (Array.isArray(parsedData)) {
607
+ const instances = parsedData.map((d) => buildInstance(d));
608
+ return { hit: true, data: instances };
609
+ }
610
+ else {
611
+ const instance = buildInstance(parsedData);
612
+ return { hit: true, data: instance };
613
+ }
614
+ }
615
+ catch (err) {
616
+ this._trackRedisError(err);
617
+ return { hit: false, data: undefined };
618
+ }
619
+ }
620
+ static async _redisClearCache(cacheKey) {
621
+ if (!this.redis)
622
+ return;
623
+ try {
624
+ await this.redis.del(cacheKey);
625
+ this.cacheStats.clears++;
626
+ }
627
+ catch (err) {
628
+ this._trackRedisError(err);
629
+ }
630
+ }
631
+ static async invalidateByTags(tags) {
632
+ if (!this.isRedisConnected ||
633
+ !Array.isArray(tags) ||
634
+ tags.length === 0 ||
635
+ !this.redis)
636
+ return;
637
+ try {
638
+ const keysToDelete = await this.redis.sunion(tags);
639
+ if (keysToDelete && keysToDelete.length > 0) {
640
+ this.logger.info(`🎯 [SNIPER] Invalidating ${keysToDelete.length} keys for tags: ${tags.join(', ')}`);
641
+ await this.redis.multi().del(keysToDelete).del(tags).exec();
642
+ }
643
+ else {
644
+ await this.redis.del(tags);
645
+ }
646
+ }
647
+ catch (err) {
648
+ this._trackRedisError(err);
649
+ }
650
+ }
651
+ static _mapSetCacheEntry(cacheKey, data, ttl) {
652
+ if (this.isShardMode)
653
+ return;
654
+ if (data === null) {
655
+ this.localNegativeCache.add(cacheKey);
656
+ this.localCache.delete(cacheKey);
657
+ }
658
+ else {
659
+ let plainData = data;
660
+ if (data && typeof data.toJSON === 'function') {
661
+ plainData = data.toJSON();
662
+ }
663
+ else if (Array.isArray(data)) {
664
+ plainData = data.map((item) => item && typeof item.toJSON === 'function' ? item.toJSON() : item);
665
+ }
666
+ const dataCopy = plainData === null
667
+ ? NEGATIVE_CACHE_PLACEHOLDER
668
+ : safeStringify(plainData, this.logger);
669
+ this.localCache.set(cacheKey, {
670
+ data: dataCopy,
671
+ expires: Date.now() + ttl,
672
+ });
673
+ this.localNegativeCache.delete(cacheKey);
674
+ }
675
+ this.cacheStats.sets++;
676
+ }
677
+ static _mapGetCachedEntry(cacheKey, includeOptions) {
678
+ if (this.isShardMode)
679
+ return { hit: false, data: undefined };
680
+ if (this.localNegativeCache.has(cacheKey)) {
681
+ this.cacheStats.mapHits++;
682
+ return { hit: true, data: null };
683
+ }
684
+ const entry = this.localCache.get(cacheKey);
685
+ if (entry && entry.expires > Date.now()) {
686
+ this.cacheStats.mapHits++;
687
+ const dataRaw = entry.data;
688
+ let parsedData;
689
+ if (typeof dataRaw === 'string') {
690
+ parsedData = safeParse(dataRaw, this.logger);
691
+ }
692
+ else {
693
+ parsedData = dataRaw;
694
+ }
695
+ if (typeof parsedData !== 'object' || parsedData === null) {
696
+ return { hit: true, data: parsedData };
697
+ }
698
+ const includeAsArray = includeOptions
699
+ ? Array.isArray(includeOptions)
700
+ ? includeOptions
701
+ : [includeOptions]
702
+ : undefined;
703
+ if (Array.isArray(parsedData)) {
704
+ const instances = this.bulkBuild(parsedData, {
705
+ isNewRecord: false,
706
+ include: includeAsArray,
707
+ });
708
+ return { hit: true, data: instances };
709
+ }
710
+ else {
711
+ const instance = this.build(parsedData, {
712
+ isNewRecord: false,
713
+ include: includeAsArray,
714
+ });
715
+ return { hit: true, data: instance };
716
+ }
717
+ }
718
+ if (entry)
719
+ this.localCache.delete(cacheKey);
720
+ return { hit: false, data: undefined };
721
+ }
722
+ static _mapClearCache(cacheKey) {
723
+ if (this.isShardMode)
724
+ return;
725
+ this.localCache.delete(cacheKey);
726
+ this.localNegativeCache.delete(cacheKey);
727
+ this.cacheStats.clears++;
728
+ }
729
+ static _mapClearAllModelCache() {
730
+ if (this.isShardMode)
731
+ return;
732
+ const prefix = `${this.CACHE_VERSION}:${this.name}:`;
733
+ let cleared = 0;
734
+ for (const key of this.localCache.keys()) {
735
+ if (key.startsWith(prefix)) {
736
+ this.localCache.delete(key);
737
+ cleared++;
738
+ }
739
+ }
740
+ for (const key of this.localNegativeCache.keys()) {
741
+ if (key.startsWith(prefix)) {
742
+ this.localNegativeCache.delete(key);
743
+ cleared++;
744
+ }
745
+ }
746
+ if (cleared > 0) {
747
+ this.logger.info(`♻️ [MAP CACHE] Cleared ${cleared} in-memory entries for ${this.name} (Redis fallback).`);
748
+ }
749
+ }
750
+ static _normalizeFindOptions(options) {
751
+ if (!options ||
752
+ typeof options !== 'object' ||
753
+ Object.keys(options).length === 0)
754
+ return { where: {} };
755
+ if (options.where) {
756
+ const sequelizeOptions = { ...options };
757
+ delete sequelizeOptions.cacheTags;
758
+ delete sequelizeOptions.noCache;
759
+ return sequelizeOptions;
760
+ }
761
+ const knownOptions = [
762
+ 'order',
763
+ 'limit',
764
+ 'attributes',
765
+ 'include',
766
+ 'group',
767
+ 'having',
768
+ ];
769
+ const cacheSpecificOptions = ['cacheTags', 'noCache'];
770
+ const whereClause = {};
771
+ const otherOptions = {};
772
+ for (const key in options) {
773
+ if (cacheSpecificOptions.includes(key)) {
774
+ continue;
775
+ }
776
+ if (knownOptions.includes(key))
777
+ otherOptions[key] = options[key];
778
+ else
779
+ whereClause[key] = options[key];
780
+ }
781
+ return { where: whereClause, ...otherOptions };
782
+ }
783
+ static async getCache(keys, options = {}) {
784
+ const { noCache, customCacheKey, ttl, ...explicitQueryOptions } = options;
785
+ if (Array.isArray(keys)) {
786
+ const pk = this.primaryKeyAttribute;
787
+ return this.findAll({ where: { [pk]: keys.map((m) => m[pk]) } });
788
+ }
789
+ const normalizedKeys = this._normalizeFindOptions(keys);
790
+ const finalQuery = {
791
+ ...normalizedKeys,
792
+ ...explicitQueryOptions,
793
+ where: {
794
+ ...(normalizedKeys.where || {}),
795
+ ...(explicitQueryOptions.where || {}),
796
+ },
797
+ };
798
+ if (noCache) {
799
+ return this.findOne(finalQuery);
800
+ }
801
+ if (!finalQuery.where || Object.keys(finalQuery.where).length === 0) {
802
+ return null;
803
+ }
804
+ const cacheKey = customCacheKey || this.getCacheKey(finalQuery);
805
+ const cacheResult = await this.getCachedEntry(cacheKey, finalQuery.include);
806
+ if (cacheResult.hit)
807
+ return cacheResult.data;
808
+ this.cacheStats.misses++;
809
+ if (this.pendingQueries.has(cacheKey))
810
+ return this.pendingQueries.get(cacheKey);
811
+ const queryPromise = this.findOne(finalQuery)
812
+ .then(async (record) => {
813
+ if (this.isRedisConnected || !this.isShardMode) {
814
+ const tags = [`${this.name}`];
815
+ if (record)
816
+ tags.push(`${this.name}:${this.primaryKeyAttribute}:${record[this.primaryKeyAttribute]}`);
817
+ await this.setCacheEntry(cacheKey, record, ttl, tags);
818
+ }
819
+ return record;
820
+ })
821
+ .finally(() => this.pendingQueries.delete(cacheKey));
822
+ this.pendingQueries.set(cacheKey, queryPromise);
823
+ return queryPromise;
824
+ }
825
+ static async getAllCache(options = {}) {
826
+ const { cacheTags, noCache, customCacheKey, ttl, ...explicitQueryOptions } = options || {};
827
+ const normalizedOptions = this._normalizeFindOptions(explicitQueryOptions);
828
+ if (noCache) {
829
+ return this.findAll(normalizedOptions);
830
+ }
831
+ const cacheKey = customCacheKey || this.getCacheKey(normalizedOptions);
832
+ const cacheResult = await this.getCachedEntry(cacheKey, normalizedOptions.include);
833
+ if (cacheResult.hit)
834
+ return cacheResult.data;
835
+ this.cacheStats.misses++;
836
+ if (this.pendingQueries.has(cacheKey))
837
+ return this.pendingQueries.get(cacheKey);
838
+ const queryPromise = this.findAll(normalizedOptions)
839
+ .then(async (records) => {
840
+ if (this.isRedisConnected || !this.isShardMode) {
841
+ const tags = [`${this.name}`];
842
+ if (Array.isArray(cacheTags))
843
+ tags.push(...cacheTags);
844
+ await this.setCacheEntry(cacheKey, records, ttl, tags);
845
+ }
846
+ return records;
847
+ })
848
+ .finally(() => this.pendingQueries.delete(cacheKey));
849
+ this.pendingQueries.set(cacheKey, queryPromise);
850
+ return queryPromise;
851
+ }
852
+ static async scheduleAdd(keySuffix, score, value) {
853
+ if (!this.isRedisConnected || !this.redis)
854
+ return;
855
+ const key = `${this.name}:${keySuffix}`;
856
+ try {
857
+ await this.redis.zadd(key, score, value);
858
+ }
859
+ catch (e) {
860
+ this._trackRedisError(e);
861
+ }
862
+ }
863
+ static async scheduleRemove(keySuffix, value) {
864
+ if (!this.isRedisConnected || !this.redis)
865
+ return;
866
+ const key = `${this.name}:${keySuffix}`;
867
+ try {
868
+ await this.redis.zrem(key, value);
869
+ }
870
+ catch (e) {
871
+ this._trackRedisError(e);
872
+ }
873
+ }
874
+ static async scheduleGetExpired(keySuffix, scoreLimit = Date.now()) {
875
+ if (!this.isRedisConnected || !this.redis)
876
+ return [];
877
+ const key = `${this.name}:${keySuffix}`;
878
+ try {
879
+ return await this.redis.zrangebyscore(key, 0, scoreLimit);
880
+ }
881
+ catch (e) {
882
+ this._trackRedisError(e);
883
+ return [];
884
+ }
885
+ }
886
+ static async scheduleClear(keySuffix) {
887
+ if (!this.isRedisConnected || !this.redis)
888
+ return;
889
+ const key = `${this.name}:${keySuffix}`;
890
+ try {
891
+ await this.redis.del(key);
892
+ }
893
+ catch (e) {
894
+ this._trackRedisError(e);
895
+ }
896
+ }
897
+ static async findOrCreateWithCache(options) {
898
+ if (!options || !options.where) {
899
+ throw new Error("findOrCreateWithCache requires a 'where' option.");
900
+ }
901
+ const { where, defaults, noCache, ...otherOptions } = options;
902
+ if (noCache) {
903
+ return this.findOrCreate(options);
904
+ }
905
+ const normalizedWhere = this._normalizeFindOptions(where).where;
906
+ const cacheKey = this.getCacheKey(normalizedWhere);
907
+ const cacheResult = await this.getCachedEntry(cacheKey, otherOptions.include);
908
+ if (cacheResult.hit && cacheResult.data) {
909
+ const instance = cacheResult.data;
910
+ let needsUpdate = false;
911
+ if (defaults && typeof defaults === 'object') {
912
+ for (const key in defaults) {
913
+ if (instance[key] === undefined ||
914
+ String(instance[key]) !== String(defaults[key])) {
915
+ instance[key] = defaults[key];
916
+ needsUpdate = true;
917
+ }
918
+ }
919
+ }
920
+ if (needsUpdate) {
921
+ await instance.saveAndUpdateCache();
922
+ }
923
+ return [instance, false];
924
+ }
925
+ this.cacheStats.misses++;
926
+ if (this.pendingQueries.has(cacheKey)) {
927
+ return this.pendingQueries.get(cacheKey);
928
+ }
929
+ const findPromise = this.findOne({ where, ...otherOptions })
930
+ .then(async (instance) => {
931
+ if (instance) {
932
+ let needsUpdate = false;
933
+ if (defaults && typeof defaults === 'object') {
934
+ for (const key in defaults) {
935
+ if (instance[key] === undefined ||
936
+ String(instance[key]) !== String(defaults[key])) {
937
+ instance[key] = defaults[key];
938
+ needsUpdate = true;
939
+ }
940
+ }
941
+ }
942
+ if (needsUpdate) {
943
+ await instance.saveAndUpdateCache();
944
+ }
945
+ else {
946
+ const tags = [
947
+ `${this.name}`,
948
+ `${this.name}:${this.primaryKeyAttribute}:${instance[this.primaryKeyAttribute]}`,
949
+ ];
950
+ await this.setCacheEntry(cacheKey, instance, undefined, tags);
951
+ }
952
+ return [instance, false];
953
+ }
954
+ else {
955
+ const createData = { ...where, ...defaults };
956
+ const newInstance = await this.create(createData);
957
+ const tags = [
958
+ `${this.name}`,
959
+ `${this.name}:${this.primaryKeyAttribute}:${newInstance[this.primaryKeyAttribute]}`,
960
+ ];
961
+ await this.setCacheEntry(cacheKey, newInstance, undefined, tags);
962
+ return [newInstance, true];
963
+ }
964
+ })
965
+ .finally(() => {
966
+ this.pendingQueries.delete(cacheKey);
967
+ });
968
+ this.pendingQueries.set(cacheKey, findPromise);
969
+ return findPromise;
970
+ }
971
+ static async countWithCache(options = {}, ttl = 5 * 60 * 1000) {
972
+ const { ...countOptions } = options || {};
973
+ const cacheKeyOptions = { queryType: 'count', ...countOptions };
974
+ const cacheKey = this.getCacheKey(cacheKeyOptions);
975
+ const cacheResult = await this.getCachedEntry(cacheKey);
976
+ if (cacheResult.hit) {
977
+ return cacheResult.data;
978
+ }
979
+ this.cacheStats.misses++;
980
+ const count = await this.count(countOptions);
981
+ if (this.isRedisConnected || !this.isShardMode) {
982
+ const tags = [`${this.name}`];
983
+ this.setCacheEntry(cacheKey, count, ttl, tags);
984
+ }
985
+ return count;
986
+ }
987
+ async saveAndUpdateCache() {
988
+ const savedInstance = await this.save();
989
+ const pk = this.constructor.primaryKeyAttribute;
990
+ const pkValue = this[pk];
991
+ if (pkValue &&
992
+ (this.constructor.isRedisConnected ||
993
+ !this.constructor.isShardMode)) {
994
+ const cacheKey = this.constructor.getCacheKey({
995
+ where: { [pk]: pkValue },
996
+ });
997
+ const tags = [
998
+ `${this.constructor.name}`,
999
+ `${this.constructor.name}:${pk}:${pkValue}`,
1000
+ ];
1001
+ await this.constructor.setCacheEntry(cacheKey, savedInstance, undefined, tags);
1002
+ this.constructor.logger.info(`🔄 [CACHE] Updated cache for ${this.constructor.name}:${pk}:${pkValue}`);
1003
+ }
1004
+ return savedInstance;
1005
+ }
1006
+ static async clearNegativeCache(keys) {
1007
+ return this.clearCache(keys);
1008
+ }
1009
+ static async aggregateWithCache(options = {}, cacheOptions = {}) {
1010
+ const { cacheTags, ...queryOptions } = options || {};
1011
+ const { ttl = 5 * 60 * 1000 } = cacheOptions || {};
1012
+ const cacheKeyOptions = { queryType: 'aggregate', ...queryOptions };
1013
+ const cacheKey = this.getCacheKey(cacheKeyOptions);
1014
+ const cacheResult = await this.getCachedEntry(cacheKey);
1015
+ if (cacheResult.hit) {
1016
+ return cacheResult.data;
1017
+ }
1018
+ this.cacheStats.misses++;
1019
+ const result = await this.findAll(queryOptions);
1020
+ if (this.isRedisConnected || !this.isShardMode) {
1021
+ const tags = [`${this.name}`];
1022
+ if (Array.isArray(cacheTags))
1023
+ tags.push(...cacheTags);
1024
+ this.setCacheEntry(cacheKey, result, ttl, tags);
1025
+ }
1026
+ return result;
1027
+ }
1028
+ static initializeCacheHooks() {
1029
+ if (!this.redis) {
1030
+ this.logger.warn(`❌ Redis not initialized for model ${this.name}. Cache hooks will not be attached.`);
1031
+ return;
1032
+ }
1033
+ const afterSaveLogic = async (instance) => {
1034
+ const modelClass = instance.constructor;
1035
+ if (modelClass.isRedisConnected) {
1036
+ const tagsToInvalidate = modelClass._generateSmartTags(instance);
1037
+ if (Array.isArray(modelClass.customInvalidationTags)) {
1038
+ tagsToInvalidate.push(...modelClass.customInvalidationTags);
1039
+ }
1040
+ await modelClass.invalidateByTags(tagsToInvalidate);
1041
+ }
1042
+ else if (!modelClass.isShardMode) {
1043
+ modelClass._mapClearAllModelCache();
1044
+ }
1045
+ };
1046
+ const afterDestroyLogic = async (instance) => {
1047
+ const modelClass = instance.constructor;
1048
+ if (modelClass.isRedisConnected) {
1049
+ const tagsToInvalidate = modelClass._generateSmartTags(instance);
1050
+ if (Array.isArray(modelClass.customInvalidationTags)) {
1051
+ tagsToInvalidate.push(...modelClass.customInvalidationTags);
1052
+ }
1053
+ await modelClass.invalidateByTags(tagsToInvalidate);
1054
+ }
1055
+ else if (!modelClass.isShardMode) {
1056
+ modelClass._mapClearAllModelCache();
1057
+ }
1058
+ };
1059
+ const afterBulkLogic = async () => {
1060
+ if (this.isRedisConnected) {
1061
+ await this.invalidateByTags([`${this.name}`]);
1062
+ }
1063
+ else if (!this.isShardMode) {
1064
+ this._mapClearAllModelCache();
1065
+ }
1066
+ };
1067
+ this.addHook('afterSave', afterSaveLogic);
1068
+ this.addHook('afterDestroy', afterDestroyLogic);
1069
+ this.addHook('afterBulkCreate', afterBulkLogic);
1070
+ this.addHook('afterBulkUpdate', afterBulkLogic);
1071
+ this.addHook('afterBulkDestroy', afterBulkLogic);
1072
+ }
1073
+ static attachHooksToAllModels(sequelizeInstance, client) {
1074
+ if (!this.redis) {
1075
+ this.logger.error('❌ Cannot attach hooks because Redis is not initialized.');
1076
+ return;
1077
+ }
1078
+ for (const modelName in sequelizeInstance.models) {
1079
+ const model = sequelizeInstance.models[modelName];
1080
+ if (model.prototype instanceof KythiaModel) {
1081
+ model.client = client;
1082
+ this.logger.info(`⚙️ Attaching hooks to ${model.name}`);
1083
+ model.initializeCacheHooks();
1084
+ }
1085
+ }
1086
+ }
1087
+ static async touchParent(childInstance, foreignKeyField, ParentModel, timestampField = 'updatedAt') {
1088
+ if (!childInstance || !childInstance[foreignKeyField]) {
1089
+ return;
1090
+ }
1091
+ try {
1092
+ const parentPk = ParentModel.primaryKeyAttribute;
1093
+ const parent = await ParentModel.findByPk(childInstance[foreignKeyField]);
1094
+ if (parent) {
1095
+ parent.changed(timestampField, true);
1096
+ await parent.save({ fields: [timestampField] });
1097
+ this.logger.info(`🔄 Touched parent ${ParentModel.name} #${parent[parentPk]} due to change in ${this.name}.`);
1098
+ }
1099
+ }
1100
+ catch (e) {
1101
+ this.logger.error(`🔄 Failed to touch parent ${ParentModel.name}`, e);
1102
+ }
1103
+ }
1104
+ static setupParentTouch(foreignKeyField, ParentModel, timestampField = 'updatedAt') {
1105
+ const touchHandler = (instance) => {
1106
+ return this.touchParent(instance, foreignKeyField, ParentModel, timestampField);
1107
+ };
1108
+ const bulkTouchHandler = (instances) => {
1109
+ if (instances && instances.length > 0) {
1110
+ return this.touchParent(instances[0], foreignKeyField, ParentModel, timestampField);
1111
+ }
1112
+ return Promise.resolve();
1113
+ };
1114
+ this.addHook('afterSave', touchHandler);
1115
+ this.addHook('afterDestroy', touchHandler);
1116
+ this.addHook('afterBulkCreate', bulkTouchHandler);
1117
+ }
1118
+ }
1119
+ exports.KythiaModel = KythiaModel;
1120
+ exports.default = KythiaModel;
1121
+ //# sourceMappingURL=KythiaModel.js.map