adminizer 4.6.0 → 4.7.0-build.222

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 (335) hide show
  1. package/Readme.md +12 -0
  2. package/assets/RelationDialogStack-CnHFOYyR.css +1 -0
  3. package/assets/RelationDialogStack-DI2bZxQX.js +20 -0
  4. package/assets/add-DCQ7ZW4B.js +1 -0
  5. package/assets/add-group-DCVPryBo.js +1 -0
  6. package/assets/add-group-form-DKRXC2n-.js +1 -0
  7. package/assets/add-user-B7OOiYxv.js +1 -0
  8. package/assets/add-user-form-UXAToWQd.js +1 -0
  9. package/assets/app-AKbNCggP.css +1 -0
  10. package/assets/app-layout-6EwNBvnR.js +1 -0
  11. package/assets/app-shell-D8wtkECO.js +1 -0
  12. package/assets/app-sidebar-DfGjL-2K.js +1 -0
  13. package/assets/app-sidebar-header-BZbIBQjq.js +1 -0
  14. package/assets/app.js +59 -7990
  15. package/assets/breadcrumbs-CODaqVkB.js +1 -0
  16. package/assets/catalog-DVvFGa2X.js +6 -0
  17. package/assets/dashboard-BadoiH_a.js +3 -0
  18. package/assets/date-picker-DSOkNiNo.js +1 -0
  19. package/assets/del-modal-Cd4M7af8.js +1 -0
  20. package/assets/dynamic-controls-BsrJhPDY.js +1 -0
  21. package/assets/geo-json-CzR5JBXL.js +1 -0
  22. package/assets/handsontable-BI98OF5R.js +336 -0
  23. package/assets/history-D1iLA6rs.js +1 -0
  24. package/assets/home-DbWj-W_k.js +2 -0
  25. package/assets/icon-C2JNaLxH.js +1 -0
  26. package/assets/init-user-D2HSyXup.js +1 -0
  27. package/assets/input-error-CH2caTtO.js +1 -0
  28. package/assets/jsoneditor-DVNQmA6G.js +3869 -0
  29. package/assets/list-CVoljVUw.js +4 -0
  30. package/assets/login-DFSLEO17.js +1 -0
  31. package/assets/manifest.json +246 -273
  32. package/assets/material-icon-Bx9pGQiQ.js +1 -0
  33. package/assets/media-manager-WcKe1EvD.js +6 -0
  34. package/assets/module-DJsC37CM.js +1 -0
  35. package/assets/monaco-Bl4zdWCn.js +11 -0
  36. package/assets/nav-footer-BiSQkPt4.js +1 -0
  37. package/assets/nav-main-rTH0PfDw.js +1 -0
  38. package/assets/nav-user-Bj6cWs0a.js +1 -0
  39. package/assets/notification-DKrNfaAB.js +20 -0
  40. package/assets/pagination-render-AKJvTIRA.js +1 -0
  41. package/assets/register-BTP4uxsK.js +1 -0
  42. package/assets/ru-eW0WKkga.js +1 -0
  43. package/assets/theme-switcher-Dm8EOGzx.js +1 -0
  44. package/assets/toast-editor-DOg_gvSp.js +668 -0
  45. package/assets/user-filters-list-C9R9J6Om.js +1 -0
  46. package/assets/user-info-BgSbT02v.js +1 -0
  47. package/assets/user-menu-content-TQTXHIv6.js +1 -0
  48. package/assets/{vanilla-picker-l5rcX3cq.js → vanilla-picker-B3LgXoKV.js} +1 -7
  49. package/assets/welcome-BwDlAOGh.js +1 -0
  50. package/controllers/add.d.ts +1 -1
  51. package/controllers/add.js +20 -26
  52. package/controllers/addGroup.d.ts +1 -1
  53. package/controllers/addGroup.js +7 -15
  54. package/controllers/addUser.d.ts +1 -1
  55. package/controllers/addUser.js +10 -16
  56. package/controllers/catalog/Catalog.d.ts +1 -1
  57. package/controllers/catalog/Catalog.js +1 -10
  58. package/controllers/catalog/FrontendCatalogAdapter.d.ts +3 -3
  59. package/controllers/catalog/FrontendCatalogAdapter.js +0 -6
  60. package/controllers/catalog/FrontentCatalogAdapter.d.ts +3 -3
  61. package/controllers/catalog/FrontentCatalogAdapter.js +0 -4
  62. package/controllers/ckeditorUpload.js +2 -18
  63. package/controllers/dashboard.d.ts +1 -1
  64. package/controllers/dashboard.js +2 -5
  65. package/controllers/edit.js +24 -37
  66. package/controllers/editGroup.d.ts +1 -1
  67. package/controllers/editGroup.js +10 -21
  68. package/controllers/editUser.d.ts +1 -1
  69. package/controllers/editUser.js +11 -23
  70. package/controllers/exportData.d.ts +6 -0
  71. package/controllers/exportData.js +315 -0
  72. package/controllers/feed.d.ts +6 -0
  73. package/controllers/feed.js +73 -0
  74. package/controllers/feedback/feedbackController.d.ts +1 -1
  75. package/controllers/feedback/feedbackController.js +5 -7
  76. package/controllers/filter-fields/columns.d.ts +11 -0
  77. package/controllers/filter-fields/columns.js +106 -0
  78. package/controllers/filter-fields/filterFields.d.ts +10 -0
  79. package/controllers/filter-fields/filterFields.js +141 -0
  80. package/controllers/filter-fields/getAllUserFilters.d.ts +6 -0
  81. package/controllers/filter-fields/getAllUserFilters.js +115 -0
  82. package/controllers/filter-fields/groups.d.ts +5 -0
  83. package/controllers/filter-fields/groups.js +26 -0
  84. package/controllers/filter-fields/savedFilters.d.ts +25 -0
  85. package/controllers/filter-fields/savedFilters.js +317 -0
  86. package/controllers/getAllWidgets.d.ts +1 -1
  87. package/controllers/getAllWidgets.js +1 -9
  88. package/controllers/history-actions/HistoryController.js +6 -31
  89. package/controllers/initUser.js +6 -5
  90. package/controllers/inlineUpdate.d.ts +7 -0
  91. package/controllers/inlineUpdate.js +148 -0
  92. package/controllers/list.d.ts +1 -1
  93. package/controllers/list.js +383 -70
  94. package/controllers/listUserFilters.d.ts +5 -0
  95. package/controllers/listUserFilters.js +26 -0
  96. package/controllers/login.js +2 -2
  97. package/controllers/media-manager/mediaManagerApi.js +0 -9
  98. package/controllers/navigation/edit.js +3 -9
  99. package/controllers/notifications/NotificationController.d.ts +1 -1
  100. package/controllers/notifications/NotificationController.js +12 -36
  101. package/controllers/register.js +10 -9
  102. package/controllers/remove.js +10 -19
  103. package/controllers/timezones.d.ts +1 -1
  104. package/controllers/timezones.js +0 -6
  105. package/controllers/userApiKey.d.ts +7 -0
  106. package/controllers/userApiKey.js +36 -0
  107. package/controllers/view.js +14 -26
  108. package/controllers/welcome.d.ts +1 -1
  109. package/controllers/welcome.js +0 -4
  110. package/controllers/widgets/Action.d.ts +1 -1
  111. package/controllers/widgets/Action.js +1 -9
  112. package/controllers/widgets/Custom.d.ts +1 -1
  113. package/controllers/widgets/Custom.js +1 -9
  114. package/controllers/widgets/Info.d.ts +1 -1
  115. package/controllers/widgets/Info.js +1 -9
  116. package/controllers/widgets/filterInfo.d.ts +1 -0
  117. package/controllers/widgets/filterInfo.js +44 -0
  118. package/controllers/widgets/switch.d.ts +1 -1
  119. package/controllers/widgets/switch.js +1 -9
  120. package/controllers/widgetsDB.d.ts +1 -1
  121. package/controllers/widgetsDB.js +2 -8
  122. package/helpers/accessRightsHelper.d.ts +1 -1
  123. package/helpers/accessRightsHelper.js +5 -4
  124. package/helpers/apiKeyHelper.d.ts +5 -0
  125. package/helpers/apiKeyHelper.js +8 -0
  126. package/helpers/configHelper.js +12 -15
  127. package/helpers/controllerHelper.d.ts +14 -21
  128. package/helpers/controllerHelper.js +50 -83
  129. package/helpers/fieldsHelper.js +3 -3
  130. package/helpers/filterDatetimeHelper.d.ts +5 -0
  131. package/helpers/filterDatetimeHelper.js +84 -0
  132. package/helpers/inertiaActionsHelper.d.ts +2 -2
  133. package/helpers/inertiaActionsHelper.js +5 -4
  134. package/helpers/inertiaAddHelper.d.ts +3 -414
  135. package/helpers/inertiaAddHelper.js +39 -15
  136. package/helpers/inertiaGroupHelper.d.ts +2 -2
  137. package/helpers/inertiaGroupHelper.js +3 -3
  138. package/helpers/inertiaListHelper.d.ts +7 -3
  139. package/helpers/inertiaListHelper.js +26 -12
  140. package/helpers/inertiaMenuHelper.d.ts +1 -0
  141. package/helpers/inertiaMenuHelper.js +15 -3
  142. package/helpers/inertiaUserHelper.d.ts +3 -2
  143. package/helpers/inertiaUserHelper.js +7 -3
  144. package/helpers/menuHelper.d.ts +5 -2
  145. package/helpers/menuHelper.js +64 -12
  146. package/helpers/viteHelper.js +1 -2
  147. package/index.d.ts +6 -2
  148. package/index.js +6 -2
  149. package/interfaces/adminpanelConfig.d.ts +126 -39
  150. package/interfaces/internalModelAccess.d.ts +37 -0
  151. package/interfaces/internalModelAccess.js +1 -0
  152. package/interfaces/listQueryBuilder.d.ts +22 -0
  153. package/interfaces/listQueryBuilder.js +1 -0
  154. package/interfaces/queryCriteria.d.ts +54 -0
  155. package/interfaces/queryCriteria.js +1 -0
  156. package/interfaces/types.d.ts +1 -13
  157. package/lib/Adminizer.d.ts +4 -2
  158. package/lib/Adminizer.js +29 -10
  159. package/lib/DataAccessor.d.ts +8 -5
  160. package/lib/DataAccessor.js +89 -42
  161. package/lib/I18n.d.ts +5 -1
  162. package/lib/I18n.js +45 -19
  163. package/lib/MiddlewareManager.d.ts +8 -0
  164. package/lib/MiddlewareManager.js +53 -0
  165. package/lib/ai-assistant/OpenAiModelService.d.ts +3 -3
  166. package/lib/ai-assistant/OpenAiModelService.js +23 -24
  167. package/lib/catalog/Navigation.d.ts +1 -0
  168. package/lib/catalog/Navigation.js +12 -11
  169. package/lib/controls/jsoneditor/JsonEditor.d.ts +1 -2
  170. package/lib/controls/markdown/ToastUiEditor.d.ts +1 -2
  171. package/lib/feedback/FeedbackHandler.js +2 -2
  172. package/lib/filters/ConditionValidator.d.ts +107 -0
  173. package/lib/filters/ConditionValidator.js +483 -0
  174. package/lib/filters/CustomFilterHandler.d.ts +122 -0
  175. package/lib/filters/CustomFilterHandler.js +131 -0
  176. package/lib/filters/FilterCustomFieldHandler.d.ts +2 -0
  177. package/lib/filters/FilterCustomFieldHandler.js +2 -0
  178. package/lib/filters/FilterService.d.ts +82 -0
  179. package/lib/filters/FilterService.js +323 -0
  180. package/lib/filters/index.d.ts +6 -0
  181. package/lib/filters/index.js +4 -0
  182. package/lib/history-actions/AbstractHistoryAdapter.d.ts +5 -4
  183. package/lib/history-actions/AbstractHistoryAdapter.js +45 -30
  184. package/lib/history-actions/DefaultHistoryAdapter.d.ts +2 -0
  185. package/lib/history-actions/DefaultHistoryAdapter.js +12 -6
  186. package/lib/history-actions/HistoryHandler.js +1 -1
  187. package/lib/list-query-builder/ListQueryBuilder.d.ts +117 -0
  188. package/lib/list-query-builder/ListQueryBuilder.js +707 -0
  189. package/lib/media-manager/AbstractMediaManager.d.ts +9 -8
  190. package/lib/media-manager/DefaultMediaManager.d.ts +2 -0
  191. package/lib/media-manager/DefaultMediaManager.js +27 -15
  192. package/lib/media-manager/Items.d.ts +4 -1
  193. package/lib/media-manager/Items.js +62 -59
  194. package/lib/media-manager/MediaManagerHandler.js +1 -1
  195. package/lib/media-manager/helpers/MediaManagerHelper.d.ts +5 -1
  196. package/lib/media-manager/helpers/MediaManagerHelper.js +38 -4
  197. package/lib/model/AbstractModel.d.ts +33 -25
  198. package/lib/model/AbstractModel.js +54 -2
  199. package/lib/model/InternalModelAccessFactory.d.ts +18 -0
  200. package/lib/model/InternalModelAccessFactory.js +82 -0
  201. package/lib/model/ModelHandler.d.ts +6 -0
  202. package/lib/model/ModelHandler.js +14 -0
  203. package/lib/model/adapter/sequelize.d.ts +32 -10
  204. package/lib/model/adapter/sequelize.js +409 -62
  205. package/lib/model/adapter/typeorm.d.ts +62 -0
  206. package/lib/model/adapter/typeorm.js +777 -0
  207. package/lib/model/internalModelAccessToken.d.ts +1 -0
  208. package/lib/model/internalModelAccessToken.js +1 -0
  209. package/lib/model/internalModelRepository.d.ts +3 -0
  210. package/lib/model/internalModelRepository.js +4 -0
  211. package/lib/notifications/AbstractNotificationService.d.ts +3 -0
  212. package/lib/notifications/AbstractNotificationService.js +42 -36
  213. package/lib/notifications/GeneralNotificationService.js +37 -40
  214. package/lib/notifications/SystemNotificationService.js +33 -36
  215. package/lib/requestProcessor.js +0 -3
  216. package/lib/ui-i18n/getUiTranslations.d.ts +1 -0
  217. package/lib/ui-i18n/getUiTranslations.js +3 -0
  218. package/lib/ui-i18n/uiTranslationKeys.d.ts +6 -0
  219. package/lib/ui-i18n/uiTranslationKeys.js +268 -0
  220. package/lib/widgets/widgetHandler.d.ts +7 -2
  221. package/lib/widgets/widgetHandler.js +98 -18
  222. package/models/FilterAP.d.ts +116 -0
  223. package/models/FilterAP.js +68 -0
  224. package/models/FilterColumnAP.d.ts +32 -0
  225. package/models/FilterColumnAP.js +21 -0
  226. package/models/GroupAP.d.ts +1 -1
  227. package/models/HistoryActionsAP.d.ts +3 -3
  228. package/models/MediaManagerMetaAP.d.ts +1 -1
  229. package/models/NavigationAP.d.ts +1 -1
  230. package/models/UserAP.d.ts +6 -1
  231. package/models/UserAP.js +4 -0
  232. package/package.json +39 -32
  233. package/policies/authPolicies.d.ts +12 -0
  234. package/policies/authPolicies.js +104 -0
  235. package/policies/permissionResolvers.d.ts +12 -0
  236. package/policies/permissionResolvers.js +52 -0
  237. package/services/FeedService.d.ts +70 -0
  238. package/services/FeedService.js +334 -0
  239. package/system/Router.d.ts +1 -0
  240. package/system/Router.js +136 -59
  241. package/system/bindAccessRights.js +11 -30
  242. package/system/bindAuthorization.js +18 -18
  243. package/system/bindCors.js +16 -1
  244. package/system/bindCustomFilterHandlers.d.ts +2 -0
  245. package/system/bindCustomFilterHandlers.js +4 -0
  246. package/system/bindInertia.js +18 -61
  247. package/system/bindReqFunctions.js +10 -2
  248. package/system/bindTranslations.js +7 -3
  249. package/system/buildInternalModelAccess.d.ts +4 -0
  250. package/system/buildInternalModelAccess.js +59 -0
  251. package/system/defaults.js +18 -14
  252. package/system/routePolicies.d.ts +2 -0
  253. package/system/routePolicies.js +29 -0
  254. package/translations/en.json +151 -39
  255. package/translations/ru.json +211 -64
  256. package/ui/components/handsontable.js +13 -27
  257. package/ui/components/ui/dialog-stack.js +1 -3
  258. package/ui/components/ui/popover.js +1 -1
  259. package/ui/components/ui/sidebar.js +1 -1
  260. package/assets/RelationDialogStack-D9UVEF9s.js +0 -407
  261. package/assets/RelationDialogStack-DV8OITPR.css +0 -4
  262. package/assets/add-BlvzWQKF.js +0 -1
  263. package/assets/add-group-1Uoq9NWe.js +0 -1
  264. package/assets/add-group-form-B01gwDGu.js +0 -1
  265. package/assets/add-user-BQCAsaRy.js +0 -1
  266. package/assets/add-user-form-Cgwc9JkI.js +0 -1
  267. package/assets/app-DaOMy1nl.css +0 -72
  268. package/assets/app-layout-CAm60qoE.js +0 -1
  269. package/assets/app-shell-QWCtn9ye.js +0 -1
  270. package/assets/app-sidebar-CfaYbnSy.js +0 -1
  271. package/assets/app-sidebar-header-BsPFDhZ8.js +0 -1
  272. package/assets/breadcrumbs-CZZirDj8.js +0 -1
  273. package/assets/catalog-k5myk7fk.js +0 -6
  274. package/assets/dashboard-BkL9ybBw.js +0 -3
  275. package/assets/date-picker-B5AZ_7Q9.js +0 -1
  276. package/assets/del-modal-BoyXZloG.js +0 -1
  277. package/assets/dynamic-controls-k61T9W2P.js +0 -1
  278. package/assets/form-1WGY0YgG.js +0 -1
  279. package/assets/form-DJapG3qF.js +0 -1
  280. package/assets/geo-json-DsMUHo0l.js +0 -4
  281. package/assets/handsontable-PxYdjN8c.js +0 -298
  282. package/assets/history-CluyR-yJ.js +0 -1
  283. package/assets/home-Dug9zBzM.js +0 -2
  284. package/assets/icon-DfDLMG5f.js +0 -1
  285. package/assets/init-user-CF80MEnT.js +0 -1
  286. package/assets/input-error-DuuqFgZa.js +0 -1
  287. package/assets/jsoneditor-De1FvcYu.js +0 -3882
  288. package/assets/list-CuexshNt.js +0 -1
  289. package/assets/list-table-BsEWjTKY.js +0 -22
  290. package/assets/login-eDemYzN-.js +0 -1
  291. package/assets/material-icon-DMT8KdmE.js +0 -1
  292. package/assets/media-manager-DfYa_mT_.js +0 -14
  293. package/assets/module-Cd4Tm6uq.js +0 -1
  294. package/assets/monaco-BhBYtg3d.js +0 -11
  295. package/assets/nav-footer-D3olYxtz.js +0 -1
  296. package/assets/nav-main-BctqyzeV.js +0 -1
  297. package/assets/nav-user-Bn1Lt-BZ.js +0 -1
  298. package/assets/notification-D64qvni-.js +0 -27
  299. package/assets/pagination-render-DxD78ahJ.js +0 -1
  300. package/assets/register-349PwYbe.js +0 -1
  301. package/assets/theme-switcher-DfExsHW2.js +0 -1
  302. package/assets/toast-editor-ESK5iWPx.js +0 -845
  303. package/assets/user-info-DtBK5Kh8.js +0 -1
  304. package/assets/user-menu-content-0XYOCMaA.js +0 -1
  305. package/assets/welcome-PDp-gUei.js +0 -1
  306. package/controllers/form.d.ts +0 -1
  307. package/controllers/form.js +0 -86
  308. package/helpers/formHelper.d.ts +0 -6
  309. package/helpers/formHelper.js +0 -38
  310. package/helpers/inertiaFromHelper.d.ts +0 -11
  311. package/helpers/inertiaFromHelper.js +0 -98
  312. package/helpers/runtimeMode.d.ts +0 -1
  313. package/helpers/runtimeMode.js +0 -3
  314. package/lib/PolicyManager.d.ts +0 -8
  315. package/lib/PolicyManager.js +0 -53
  316. package/lib/datatable/NodeTable.d.ts +0 -49
  317. package/lib/datatable/NodeTable.js +0 -207
  318. package/lib/model/adapter/waterline.d.ts +0 -32
  319. package/lib/model/adapter/waterline.js +0 -180
  320. package/migrations/index.d.ts +0 -3
  321. package/migrations/index.js +0 -4
  322. package/migrations/knex/20250106000001_adminizer_init.d.ts +0 -8
  323. package/migrations/knex/20250106000001_adminizer_init.js +0 -164
  324. package/migrations/umzug/0001.d.ts +0 -7
  325. package/migrations/umzug/0001.js +0 -121
  326. package/migrations/umzugExports.d.ts +0 -2
  327. package/migrations/umzugExports.js +0 -9
  328. package/system/bindForms.d.ts +0 -2
  329. package/system/bindForms.js +0 -20
  330. /package/{policies → middlewares}/addMessages.d.ts +0 -0
  331. /package/{policies → middlewares}/addMessages.js +0 -0
  332. /package/{policies → middlewares}/checkAuth.d.ts +0 -0
  333. /package/{policies → middlewares}/checkAuth.js +0 -0
  334. /package/{policies → middlewares}/setLocale.d.ts +0 -0
  335. /package/{policies → middlewares}/setLocale.js +0 -0
@@ -0,0 +1,483 @@
1
+ import { FILTER_SECURITY_LIMITS } from '../list-query-builder/ListQueryBuilder.js';
2
+ /**
3
+ * ConditionValidator - validates filter conditions
4
+ *
5
+ * Validates:
6
+ * - Field existence
7
+ * - Operator validity for field type
8
+ * - Value type and format
9
+ * - Security limits (depth, array size, etc.)
10
+ *
11
+ * Security logging:
12
+ * Set ConditionValidator.logger to enable logging of suspicious attempts
13
+ */
14
+ export class ConditionValidator {
15
+ fieldsConfig;
16
+ allowedFields;
17
+ /**
18
+ * Static logger for security events
19
+ * Set this to enable logging of suspicious filter attempts
20
+ */
21
+ static logger = null;
22
+ /**
23
+ * Collected security events during validation
24
+ */
25
+ securityEvents = [];
26
+ constructor(fieldsConfig, allowedFields) {
27
+ this.fieldsConfig = fieldsConfig;
28
+ this.allowedFields = allowedFields;
29
+ }
30
+ /**
31
+ * Validate array of conditions
32
+ */
33
+ validate(conditions) {
34
+ const errors = [];
35
+ const context = {
36
+ totalConditions: 0,
37
+ totalConditionsExceeded: false
38
+ };
39
+ this.securityEvents = []; // Reset for new validation
40
+ for (const condition of conditions) {
41
+ this.validateCondition(condition, errors, 0, context);
42
+ if (context.totalConditionsExceeded) {
43
+ break;
44
+ }
45
+ }
46
+ return {
47
+ valid: errors.length === 0,
48
+ errors,
49
+ securityEvents: this.securityEvents.length > 0 ? this.securityEvents : undefined
50
+ };
51
+ }
52
+ /**
53
+ * Log a security event (suspicious filter attempt)
54
+ */
55
+ logSecurityEvent(type, conditionId, details, value) {
56
+ const event = {
57
+ type,
58
+ conditionId,
59
+ details,
60
+ timestamp: new Date(),
61
+ value
62
+ };
63
+ this.securityEvents.push(event);
64
+ // Log to external logger if configured
65
+ if (ConditionValidator.logger) {
66
+ ConditionValidator.logger.warn(`[SECURITY] Filter validation: ${type} - ${details}` +
67
+ (value !== undefined ? ` (value: ${JSON.stringify(value).substring(0, 100)})` : ''));
68
+ }
69
+ }
70
+ /**
71
+ * Validate single condition recursively
72
+ */
73
+ validateCondition(condition, errors, depth, context) {
74
+ if (context.totalConditionsExceeded) {
75
+ return;
76
+ }
77
+ context.totalConditions += 1;
78
+ if (context.totalConditions > FILTER_SECURITY_LIMITS.MAX_TOTAL_CONDITIONS) {
79
+ context.totalConditionsExceeded = true;
80
+ this.logSecurityEvent('TOO_MANY_TOTAL_CONDITIONS', condition.id, `Attempted ${context.totalConditions} total conditions, max ${FILTER_SECURITY_LIMITS.MAX_TOTAL_CONDITIONS}`, context.totalConditions);
81
+ errors.push({
82
+ conditionId: condition.id,
83
+ message: `Too many total conditions (max ${FILTER_SECURITY_LIMITS.MAX_TOTAL_CONDITIONS})`,
84
+ code: 'TOO_MANY_TOTAL_CONDITIONS'
85
+ });
86
+ return;
87
+ }
88
+ // Check depth limit
89
+ if (depth > FILTER_SECURITY_LIMITS.MAX_DEPTH) {
90
+ this.logSecurityEvent('MAX_DEPTH_EXCEEDED', condition.id, `Attempted nesting depth ${depth} exceeds max ${FILTER_SECURITY_LIMITS.MAX_DEPTH}`, depth);
91
+ errors.push({
92
+ conditionId: condition.id,
93
+ message: `Maximum nesting depth exceeded (max ${FILTER_SECURITY_LIMITS.MAX_DEPTH})`,
94
+ code: 'MAX_DEPTH_EXCEEDED'
95
+ });
96
+ return;
97
+ }
98
+ // Group with children
99
+ if (condition.children && condition.children.length > 0) {
100
+ // Check conditions count
101
+ if (condition.children.length > FILTER_SECURITY_LIMITS.MAX_CONDITIONS_PER_GROUP) {
102
+ this.logSecurityEvent('TOO_MANY_CONDITIONS', condition.id, `Attempted ${condition.children.length} conditions in group, max ${FILTER_SECURITY_LIMITS.MAX_CONDITIONS_PER_GROUP}`, condition.children.length);
103
+ errors.push({
104
+ conditionId: condition.id,
105
+ message: `Too many conditions in group (max ${FILTER_SECURITY_LIMITS.MAX_CONDITIONS_PER_GROUP})`,
106
+ code: 'TOO_MANY_CONDITIONS'
107
+ });
108
+ return;
109
+ }
110
+ // Validate logic operator
111
+ if (condition.logic && !['AND', 'OR', 'NOT'].includes(condition.logic)) {
112
+ errors.push({
113
+ conditionId: condition.id,
114
+ message: `Invalid logic operator: ${condition.logic}`,
115
+ code: 'INVALID_LOGIC'
116
+ });
117
+ }
118
+ // NOT requires exactly one child
119
+ if (condition.logic === 'NOT' && condition.children.length !== 1) {
120
+ errors.push({
121
+ conditionId: condition.id,
122
+ message: 'NOT operator requires exactly one condition',
123
+ code: 'NOT_REQUIRES_ONE'
124
+ });
125
+ }
126
+ // Validate children
127
+ for (const child of condition.children) {
128
+ this.validateCondition(child, errors, depth + 1, context);
129
+ }
130
+ return;
131
+ }
132
+ // Custom handler condition support
133
+ if (condition.customHandler) {
134
+ if (!condition.operator) {
135
+ errors.push({
136
+ conditionId: condition.id,
137
+ message: 'Operator is required',
138
+ code: 'OPERATOR_REQUIRED'
139
+ });
140
+ return;
141
+ }
142
+ if (condition.operator !== 'custom') {
143
+ errors.push({
144
+ conditionId: condition.id,
145
+ message: `Operator '${condition.operator}' is not allowed for custom handler`,
146
+ code: 'INVALID_OPERATOR'
147
+ });
148
+ return;
149
+ }
150
+ const customValue = condition.value;
151
+ const isEmptyCustomObject = typeof customValue === 'object'
152
+ && customValue !== null
153
+ && !Array.isArray(customValue)
154
+ && Object.values(customValue).every((v) => v === undefined
155
+ || v === null
156
+ || (typeof v === 'string' && v.trim() === ''));
157
+ if (customValue === undefined
158
+ || customValue === null
159
+ || (typeof customValue === 'string' && customValue.trim() === '')
160
+ || isEmptyCustomObject) {
161
+ errors.push({
162
+ conditionId: condition.id,
163
+ message: 'Value is required for custom filter',
164
+ code: 'INVALID_VALUE'
165
+ });
166
+ return;
167
+ }
168
+ return;
169
+ }
170
+ // Skip standard validation for raw SQL (has dedicated validation path)
171
+ if (condition.rawSQL) {
172
+ this.validateRawSQL(condition, errors);
173
+ return;
174
+ }
175
+ // Relation condition support
176
+ if (condition.relation || condition.relationField) {
177
+ if (!condition.relation || !condition.relationField) {
178
+ errors.push({
179
+ conditionId: condition.id,
180
+ message: 'Both relation and relationField are required',
181
+ code: 'RELATION_FIELDS_REQUIRED'
182
+ });
183
+ return;
184
+ }
185
+ if (!condition.operator) {
186
+ errors.push({
187
+ conditionId: condition.id,
188
+ message: 'Operator is required',
189
+ code: 'OPERATOR_REQUIRED'
190
+ });
191
+ return;
192
+ }
193
+ if (!['eq', 'neq'].includes(condition.operator)) {
194
+ errors.push({
195
+ conditionId: condition.id,
196
+ message: `Operator '${condition.operator}' is not allowed for relation conditions`,
197
+ code: 'INVALID_OPERATOR'
198
+ });
199
+ return;
200
+ }
201
+ const relationValueError = this.validateValue(condition.value, condition.operator, { type: 'string' });
202
+ if (relationValueError) {
203
+ errors.push({
204
+ conditionId: condition.id,
205
+ message: relationValueError,
206
+ code: 'INVALID_VALUE'
207
+ });
208
+ }
209
+ return;
210
+ }
211
+ // Check field presence
212
+ if (!condition.field) {
213
+ errors.push({
214
+ conditionId: condition.id,
215
+ message: 'Field is required',
216
+ code: 'FIELD_REQUIRED'
217
+ });
218
+ return;
219
+ }
220
+ // Check field is allowed
221
+ if (this.allowedFields && !this.allowedFields.includes(condition.field)) {
222
+ errors.push({
223
+ conditionId: condition.id,
224
+ field: condition.field,
225
+ message: `Field '${condition.field}' is not allowed for filtering`,
226
+ code: 'FIELD_NOT_ALLOWED'
227
+ });
228
+ return;
229
+ }
230
+ // Check field exists in config
231
+ const fieldConfig = this.fieldsConfig[condition.field];
232
+ if (!fieldConfig) {
233
+ errors.push({
234
+ conditionId: condition.id,
235
+ field: condition.field,
236
+ message: `Unknown field: ${condition.field}`,
237
+ code: 'UNKNOWN_FIELD'
238
+ });
239
+ return;
240
+ }
241
+ // Check operator presence
242
+ if (!condition.operator) {
243
+ errors.push({
244
+ conditionId: condition.id,
245
+ field: condition.field,
246
+ message: 'Operator is required',
247
+ code: 'OPERATOR_REQUIRED'
248
+ });
249
+ return;
250
+ }
251
+ // Check operator validity for field type
252
+ if (!this.isOperatorValidForType(condition.operator, fieldConfig.type)) {
253
+ errors.push({
254
+ conditionId: condition.id,
255
+ field: condition.field,
256
+ message: `Operator '${condition.operator}' is not valid for field type '${fieldConfig.type}'`,
257
+ code: 'INVALID_OPERATOR'
258
+ });
259
+ }
260
+ // Check value
261
+ const valueError = this.validateValue(condition.value, condition.operator, fieldConfig);
262
+ if (valueError) {
263
+ // Log security events for specific value errors
264
+ if (valueError.includes('Too many values')) {
265
+ this.logSecurityEvent('TOO_MANY_IN_VALUES', condition.id, `IN operator with ${Array.isArray(condition.value) ? condition.value.length : 0} values exceeds limit`, Array.isArray(condition.value) ? condition.value.length : 0);
266
+ }
267
+ else if (valueError.includes('String too long')) {
268
+ this.logSecurityEvent('STRING_TOO_LONG', condition.id, `String value length ${typeof condition.value === 'string' ? condition.value.length : 0} exceeds limit`, typeof condition.value === 'string' ? condition.value.length : 0);
269
+ }
270
+ errors.push({
271
+ conditionId: condition.id,
272
+ field: condition.field,
273
+ message: valueError,
274
+ code: 'INVALID_VALUE'
275
+ });
276
+ }
277
+ }
278
+ /**
279
+ * Check if operator is valid for field type
280
+ */
281
+ isOperatorValidForType(operator, fieldType) {
282
+ const operatorsByType = {
283
+ string: ['eq', 'neq', 'like', 'ilike', 'startsWith', 'endsWith', 'in', 'notIn', 'isNull', 'isNotNull', 'regex'],
284
+ text: ['eq', 'neq', 'like', 'ilike', 'startsWith', 'endsWith', 'isNull', 'isNotNull'],
285
+ number: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
286
+ integer: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
287
+ float: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
288
+ boolean: ['eq', 'neq', 'isNull', 'isNotNull'],
289
+ date: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'today', 'month', 'year', 'monthBetween', 'yearBetween', 'isNull', 'isNotNull'],
290
+ datetime: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'today', 'month', 'year', 'monthBetween', 'yearBetween', 'isNull', 'isNotNull'],
291
+ select: ['eq', 'neq', 'in', 'notIn', 'isNull', 'isNotNull'],
292
+ json: ['isNull', 'isNotNull', 'custom'],
293
+ association: ['eq', 'neq', 'in', 'notIn', 'isNull', 'isNotNull'],
294
+ 'association-many': ['in', 'notIn', 'isNull', 'isNotNull']
295
+ };
296
+ const allowed = operatorsByType[fieldType] || operatorsByType.string;
297
+ return allowed.includes(operator);
298
+ }
299
+ /**
300
+ * Validate value for operator and field type
301
+ */
302
+ validateValue(value, operator, fieldConfig) {
303
+ // Operators without value
304
+ if (operator === 'isNull' || operator === 'isNotNull' || operator === 'today') {
305
+ return null;
306
+ }
307
+ // Check value presence
308
+ if (value === undefined || value === null || value === '') {
309
+ return 'Value is required';
310
+ }
311
+ // between requires array of 2 elements
312
+ if (operator === 'between') {
313
+ if (!Array.isArray(value)) {
314
+ return 'BETWEEN requires array value';
315
+ }
316
+ if (value.length !== 2) {
317
+ return 'BETWEEN requires array of 2 values';
318
+ }
319
+ }
320
+ if (operator === 'month') {
321
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}$/.test(value)) {
322
+ return 'MONTH requires YYYY-MM value';
323
+ }
324
+ }
325
+ if (operator === 'year') {
326
+ if (!((typeof value === 'string' && /^\d{4}$/.test(value)) ||
327
+ (typeof value === 'number' && Number.isInteger(value)))) {
328
+ return 'YEAR requires YYYY value';
329
+ }
330
+ }
331
+ if (operator === 'monthBetween') {
332
+ if (!Array.isArray(value) || value.length !== 2) {
333
+ return 'MONTH BETWEEN requires array of 2 YYYY-MM values';
334
+ }
335
+ const [from, to] = value;
336
+ if (typeof from !== 'string' || !/^\d{4}-\d{2}$/.test(from) ||
337
+ typeof to !== 'string' || !/^\d{4}-\d{2}$/.test(to)) {
338
+ return 'MONTH BETWEEN requires array of 2 YYYY-MM values';
339
+ }
340
+ }
341
+ if (operator === 'yearBetween') {
342
+ if (!Array.isArray(value) || value.length !== 2) {
343
+ return 'YEAR BETWEEN requires array of 2 YYYY values';
344
+ }
345
+ const [from, to] = value;
346
+ const isValidYear = (v) => (typeof v === 'string' && /^\d{4}$/.test(v)) ||
347
+ (typeof v === 'number' && Number.isInteger(v));
348
+ if (!isValidYear(from) || !isValidYear(to)) {
349
+ return 'YEAR BETWEEN requires array of 2 YYYY values';
350
+ }
351
+ }
352
+ // in/notIn require array
353
+ if (operator === 'in' || operator === 'notIn') {
354
+ if (!Array.isArray(value)) {
355
+ return 'IN/NOT IN requires array of values';
356
+ }
357
+ if (value.length === 0) {
358
+ return 'IN/NOT IN requires at least one value';
359
+ }
360
+ if (value.length > FILTER_SECURITY_LIMITS.MAX_IN_VALUES) {
361
+ // Note: We can't call logSecurityEvent from validateValue since it returns string
362
+ // Security logging for this is handled in validateCondition
363
+ return `Too many values (max ${FILTER_SECURITY_LIMITS.MAX_IN_VALUES})`;
364
+ }
365
+ }
366
+ // regex requires valid pattern
367
+ if (operator === 'regex') {
368
+ if (typeof value !== 'string') {
369
+ return 'Regex requires string pattern';
370
+ }
371
+ try {
372
+ new RegExp(value);
373
+ }
374
+ catch (e) {
375
+ return `Invalid regex pattern: ${e?.message || e}`;
376
+ }
377
+ }
378
+ // String length check
379
+ if (typeof value === 'string') {
380
+ if (value.length > FILTER_SECURITY_LIMITS.MAX_STRING_LENGTH) {
381
+ return `String too long (max ${FILTER_SECURITY_LIMITS.MAX_STRING_LENGTH} characters)`;
382
+ }
383
+ if (fieldConfig.maxLength && value.length > fieldConfig.maxLength) {
384
+ return `String exceeds field max length of ${fieldConfig.maxLength}`;
385
+ }
386
+ }
387
+ // Number range check
388
+ if (typeof value === 'number') {
389
+ if (fieldConfig.min !== undefined && value < fieldConfig.min) {
390
+ return `Value must be at least ${fieldConfig.min}`;
391
+ }
392
+ if (fieldConfig.max !== undefined && value > fieldConfig.max) {
393
+ return `Value must be at most ${fieldConfig.max}`;
394
+ }
395
+ }
396
+ // Type validation for specific field types
397
+ if (fieldConfig.type === 'boolean') {
398
+ if (typeof value !== 'boolean' && value !== 'true' && value !== 'false') {
399
+ return 'Value must be boolean';
400
+ }
401
+ }
402
+ if (fieldConfig.type === 'number' || fieldConfig.type === 'integer' || fieldConfig.type === 'float') {
403
+ if (typeof value !== 'number' && isNaN(parseFloat(value))) {
404
+ return 'Value must be a number';
405
+ }
406
+ }
407
+ return null;
408
+ }
409
+ /**
410
+ * Validate raw SQL for obvious injection attempts
411
+ */
412
+ validateRawSQL(condition, errors) {
413
+ const sql = condition.rawSQL;
414
+ if (!sql)
415
+ return;
416
+ // Check for dangerous patterns
417
+ const dangerousPatterns = [
418
+ /;\s*drop\s+/i,
419
+ /;\s*delete\s+/i,
420
+ /;\s*update\s+/i,
421
+ /;\s*insert\s+/i,
422
+ /;\s*alter\s+/i,
423
+ /;\s*create\s+/i,
424
+ /;\s*truncate\s+/i,
425
+ /union\s+select/i,
426
+ /--\s*$/m,
427
+ /\/\*.*\*\//s
428
+ ];
429
+ for (const pattern of dangerousPatterns) {
430
+ if (pattern.test(sql)) {
431
+ this.logSecurityEvent('DANGEROUS_SQL', condition.id, `SQL injection attempt detected: pattern ${pattern.toString()} matched`, sql.substring(0, 200));
432
+ errors.push({
433
+ conditionId: condition.id,
434
+ message: 'Raw SQL contains potentially dangerous pattern',
435
+ code: 'DANGEROUS_SQL'
436
+ });
437
+ return;
438
+ }
439
+ }
440
+ // Check that params placeholders match params count
441
+ if (condition.rawSQLParams) {
442
+ const placeholderCount = (sql.match(/\$\d+|\?/g) || []).length;
443
+ if (placeholderCount !== condition.rawSQLParams.length) {
444
+ errors.push({
445
+ conditionId: condition.id,
446
+ message: `Placeholder count (${placeholderCount}) doesn't match params count (${condition.rawSQLParams.length})`,
447
+ code: 'PARAM_MISMATCH'
448
+ });
449
+ }
450
+ }
451
+ }
452
+ /**
453
+ * Get list of allowed operators for a field
454
+ */
455
+ getOperatorsForField(fieldName) {
456
+ const fieldConfig = this.fieldsConfig[fieldName];
457
+ if (!fieldConfig) {
458
+ return [];
459
+ }
460
+ return this.getOperatorsForType(fieldConfig.type);
461
+ }
462
+ /**
463
+ * Get list of allowed operators for a field type
464
+ */
465
+ getOperatorsForType(fieldType) {
466
+ const operatorsByType = {
467
+ string: ['eq', 'neq', 'like', 'ilike', 'startsWith', 'endsWith', 'in', 'notIn', 'isNull', 'isNotNull', 'regex'],
468
+ text: ['eq', 'neq', 'like', 'ilike', 'startsWith', 'endsWith', 'isNull', 'isNotNull'],
469
+ number: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
470
+ integer: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
471
+ float: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
472
+ boolean: ['eq', 'neq', 'isNull', 'isNotNull'],
473
+ date: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'today', 'month', 'year', 'monthBetween', 'yearBetween', 'isNull', 'isNotNull'],
474
+ datetime: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'today', 'month', 'year', 'monthBetween', 'yearBetween', 'isNull', 'isNotNull'],
475
+ select: ['eq', 'neq', 'in', 'notIn', 'isNull', 'isNotNull'],
476
+ json: ['isNull', 'isNotNull', 'custom'],
477
+ association: ['eq', 'neq', 'in', 'notIn', 'isNull', 'isNotNull'],
478
+ 'association-many': ['in', 'notIn', 'isNull', 'isNotNull']
479
+ };
480
+ return operatorsByType[fieldType] || operatorsByType.string;
481
+ }
482
+ }
483
+ export default ConditionValidator;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Custom field condition result
3
+ */
4
+ export interface FilterCustomFieldCondition {
5
+ rawSQL?: string;
6
+ params?: any[];
7
+ inMemory?: (record: any) => boolean;
8
+ criteria?: Record<string, any>;
9
+ }
10
+ export type FilterCustomFieldInputType = "text" | "number";
11
+ export interface FilterCustomFieldInputConfigItem {
12
+ placeholder: string;
13
+ type?: FilterCustomFieldInputType;
14
+ }
15
+ /**
16
+ * Input config provided by a custom filter handler class.
17
+ * Keys are input IDs; values describe placeholders and optional HTML input type.
18
+ */
19
+ export type FilterCustomFieldInputConfig = Record<string, FilterCustomFieldInputConfigItem>;
20
+ export interface FilterCustomFieldInputDefinition {
21
+ id: string;
22
+ placeholder: string;
23
+ type: FilterCustomFieldInputType;
24
+ }
25
+ /**
26
+ * Base custom filter handler contract.
27
+ * Stores model and field separately and builds a full handler ID as "Model.field".
28
+ */
29
+ export declare abstract class AbstractCustomFilter {
30
+ private readonly modelName;
31
+ private readonly fieldName;
32
+ protected constructor(modelName: string, fieldName: string);
33
+ get id(): string;
34
+ /**
35
+ * Handler name for display
36
+ */
37
+ abstract readonly name: string;
38
+ /**
39
+ * Handler description
40
+ */
41
+ readonly description?: string;
42
+ /**
43
+ * Optional input config for UI (up to 3 inputs).
44
+ * Example:
45
+ * {
46
+ * from: { placeholder: "From", type: "number" },
47
+ * to: { placeholder: "To", type: "number" }
48
+ * }
49
+ */
50
+ readonly inputConfig?: FilterCustomFieldInputConfig;
51
+ /**
52
+ * Build condition for the custom field
53
+ * @param value - Filter value
54
+ * @param params - Additional parameters for the handler
55
+ */
56
+ abstract buildCondition(value: any, params?: any): FilterCustomFieldCondition;
57
+ /**
58
+ * Validate value before applying filter
59
+ */
60
+ validate?(value: any): {
61
+ valid: boolean;
62
+ error?: string;
63
+ };
64
+ getInputDefinitions(): FilterCustomFieldInputDefinition[];
65
+ }
66
+ /**
67
+ * Registry options
68
+ */
69
+ export interface RegisterOptions {
70
+ /**
71
+ * Force overwrite existing handler
72
+ */
73
+ force?: boolean;
74
+ }
75
+ /**
76
+ * CustomFilterHandler - registry for custom field handlers
77
+ */
78
+ export declare class CustomFilterHandler {
79
+ private handlers;
80
+ private validateInputConfig;
81
+ /**
82
+ * Register a custom handler
83
+ * @param handler - Handler instance
84
+ * @param options - Registration options
85
+ */
86
+ add(handler: AbstractCustomFilter, options?: RegisterOptions): void;
87
+ /**
88
+ * Alias for add()
89
+ */
90
+ register(handler: AbstractCustomFilter, options?: RegisterOptions): void;
91
+ /**
92
+ * Get handler by ID
93
+ */
94
+ get(id: string): AbstractCustomFilter | undefined;
95
+ /**
96
+ * Check if handler exists
97
+ */
98
+ has(id: string): boolean;
99
+ /**
100
+ * Get all handlers
101
+ */
102
+ getAll(): Map<string, AbstractCustomFilter>;
103
+ /**
104
+ * Get handlers for a specific model
105
+ * @param modelName - Model name (e.g., "Order")
106
+ * @returns Map of field handlers for the model
107
+ */
108
+ getForModel(modelName: string): Map<string, AbstractCustomFilter>;
109
+ /**
110
+ * Unregister a handler
111
+ */
112
+ unregister(id: string): boolean;
113
+ /**
114
+ * Clear all handlers (useful for testing)
115
+ */
116
+ clear(): void;
117
+ /**
118
+ * Get count of registered handlers
119
+ */
120
+ count(): number;
121
+ }
122
+ export default CustomFilterHandler;