adminizer 4.6.0 → 4.7.0-build.221
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +12 -0
- package/assets/RelationDialogStack-CnHFOYyR.css +1 -0
- package/assets/RelationDialogStack-DI2bZxQX.js +20 -0
- package/assets/add-4213XScp.js +1 -0
- package/assets/add-group--F1XxfJ1.js +1 -0
- package/assets/add-group-form-DKRXC2n-.js +1 -0
- package/assets/add-user-b9HfMq7z.js +1 -0
- package/assets/add-user-form-UXAToWQd.js +1 -0
- package/assets/app-Clx8f5hd.css +1 -0
- package/assets/app-layout-C02ja4OI.js +1 -0
- package/assets/app-shell-D8wtkECO.js +1 -0
- package/assets/app-sidebar-BR5_zwxj.js +1 -0
- package/assets/app-sidebar-header-BZbIBQjq.js +1 -0
- package/assets/app.js +59 -7990
- package/assets/breadcrumbs-CODaqVkB.js +1 -0
- package/assets/catalog-6ZZjWdFs.js +6 -0
- package/assets/dashboard-CftAdKiH.js +3 -0
- package/assets/date-picker-DSOkNiNo.js +1 -0
- package/assets/del-modal-Cd4M7af8.js +1 -0
- package/assets/dynamic-controls-BsrJhPDY.js +1 -0
- package/assets/geo-json-CzR5JBXL.js +1 -0
- package/assets/handsontable-BI98OF5R.js +336 -0
- package/assets/history-DLHSU7Ak.js +1 -0
- package/assets/home-NcQ7nnU0.js +2 -0
- package/assets/icon-C2JNaLxH.js +1 -0
- package/assets/init-user-D2HSyXup.js +1 -0
- package/assets/input-error-CH2caTtO.js +1 -0
- package/assets/jsoneditor-DVNQmA6G.js +3869 -0
- package/assets/list-CFU0-TgN.js +4 -0
- package/assets/login-DFSLEO17.js +1 -0
- package/assets/manifest.json +246 -273
- package/assets/material-icon-Bx9pGQiQ.js +1 -0
- package/assets/media-manager-WcKe1EvD.js +6 -0
- package/assets/module-CT7PC6pc.js +1 -0
- package/assets/monaco-Bl4zdWCn.js +11 -0
- package/assets/nav-footer-BiSQkPt4.js +1 -0
- package/assets/nav-main-rTH0PfDw.js +1 -0
- package/assets/nav-user-Bj6cWs0a.js +1 -0
- package/assets/notification-Bi-PX87D.js +20 -0
- package/assets/pagination-render-AKJvTIRA.js +1 -0
- package/assets/register-BTP4uxsK.js +1 -0
- package/assets/ru-eW0WKkga.js +1 -0
- package/assets/theme-switcher-Dm8EOGzx.js +1 -0
- package/assets/toast-editor-DOg_gvSp.js +668 -0
- package/assets/user-filters-list-YUfR4XMI.js +1 -0
- package/assets/user-info-BgSbT02v.js +1 -0
- package/assets/user-menu-content-TQTXHIv6.js +1 -0
- package/assets/{vanilla-picker-l5rcX3cq.js → vanilla-picker-B3LgXoKV.js} +1 -7
- package/assets/welcome-vLwTu1R6.js +1 -0
- package/controllers/add.d.ts +1 -1
- package/controllers/add.js +20 -26
- package/controllers/addGroup.d.ts +1 -1
- package/controllers/addGroup.js +7 -15
- package/controllers/addUser.d.ts +1 -1
- package/controllers/addUser.js +10 -16
- package/controllers/catalog/Catalog.d.ts +1 -1
- package/controllers/catalog/Catalog.js +1 -10
- package/controllers/catalog/FrontendCatalogAdapter.d.ts +3 -3
- package/controllers/catalog/FrontendCatalogAdapter.js +0 -6
- package/controllers/catalog/FrontentCatalogAdapter.d.ts +3 -3
- package/controllers/catalog/FrontentCatalogAdapter.js +0 -4
- package/controllers/ckeditorUpload.js +2 -18
- package/controllers/dashboard.d.ts +1 -1
- package/controllers/dashboard.js +2 -5
- package/controllers/edit.js +24 -37
- package/controllers/editGroup.d.ts +1 -1
- package/controllers/editGroup.js +10 -21
- package/controllers/editUser.d.ts +1 -1
- package/controllers/editUser.js +11 -23
- package/controllers/exportData.d.ts +6 -0
- package/controllers/exportData.js +315 -0
- package/controllers/feed.d.ts +6 -0
- package/controllers/feed.js +73 -0
- package/controllers/feedback/feedbackController.d.ts +1 -1
- package/controllers/feedback/feedbackController.js +5 -7
- package/controllers/filter-fields/columns.d.ts +11 -0
- package/controllers/filter-fields/columns.js +106 -0
- package/controllers/filter-fields/filterFields.d.ts +10 -0
- package/controllers/filter-fields/filterFields.js +141 -0
- package/controllers/filter-fields/getAllUserFilters.d.ts +6 -0
- package/controllers/filter-fields/getAllUserFilters.js +115 -0
- package/controllers/filter-fields/groups.d.ts +5 -0
- package/controllers/filter-fields/groups.js +26 -0
- package/controllers/filter-fields/savedFilters.d.ts +25 -0
- package/controllers/filter-fields/savedFilters.js +317 -0
- package/controllers/getAllWidgets.d.ts +1 -1
- package/controllers/getAllWidgets.js +1 -9
- package/controllers/history-actions/HistoryController.js +6 -31
- package/controllers/initUser.js +6 -5
- package/controllers/inlineUpdate.d.ts +7 -0
- package/controllers/inlineUpdate.js +148 -0
- package/controllers/list.d.ts +1 -1
- package/controllers/list.js +383 -70
- package/controllers/listUserFilters.d.ts +5 -0
- package/controllers/listUserFilters.js +26 -0
- package/controllers/login.js +2 -2
- package/controllers/media-manager/mediaManagerApi.js +0 -9
- package/controllers/navigation/edit.js +3 -9
- package/controllers/notifications/NotificationController.d.ts +1 -1
- package/controllers/notifications/NotificationController.js +12 -36
- package/controllers/register.js +10 -9
- package/controllers/remove.js +10 -19
- package/controllers/timezones.d.ts +1 -1
- package/controllers/timezones.js +0 -6
- package/controllers/userApiKey.d.ts +7 -0
- package/controllers/userApiKey.js +36 -0
- package/controllers/view.js +14 -26
- package/controllers/welcome.d.ts +1 -1
- package/controllers/welcome.js +0 -4
- package/controllers/widgets/Action.d.ts +1 -1
- package/controllers/widgets/Action.js +1 -9
- package/controllers/widgets/Custom.d.ts +1 -1
- package/controllers/widgets/Custom.js +1 -9
- package/controllers/widgets/Info.d.ts +1 -1
- package/controllers/widgets/Info.js +1 -9
- package/controllers/widgets/filterInfo.d.ts +1 -0
- package/controllers/widgets/filterInfo.js +44 -0
- package/controllers/widgets/switch.d.ts +1 -1
- package/controllers/widgets/switch.js +1 -9
- package/controllers/widgetsDB.d.ts +1 -1
- package/controllers/widgetsDB.js +2 -8
- package/helpers/accessRightsHelper.d.ts +1 -1
- package/helpers/accessRightsHelper.js +5 -4
- package/helpers/apiKeyHelper.d.ts +5 -0
- package/helpers/apiKeyHelper.js +8 -0
- package/helpers/configHelper.js +12 -15
- package/helpers/controllerHelper.d.ts +14 -21
- package/helpers/controllerHelper.js +50 -83
- package/helpers/fieldsHelper.js +3 -3
- package/helpers/filterDatetimeHelper.d.ts +5 -0
- package/helpers/filterDatetimeHelper.js +84 -0
- package/helpers/inertiaActionsHelper.d.ts +2 -2
- package/helpers/inertiaActionsHelper.js +5 -4
- package/helpers/inertiaAddHelper.d.ts +3 -414
- package/helpers/inertiaAddHelper.js +39 -15
- package/helpers/inertiaGroupHelper.d.ts +2 -2
- package/helpers/inertiaGroupHelper.js +3 -3
- package/helpers/inertiaListHelper.d.ts +7 -3
- package/helpers/inertiaListHelper.js +26 -12
- package/helpers/inertiaMenuHelper.d.ts +1 -0
- package/helpers/inertiaMenuHelper.js +15 -3
- package/helpers/inertiaUserHelper.d.ts +3 -2
- package/helpers/inertiaUserHelper.js +7 -3
- package/helpers/menuHelper.d.ts +5 -2
- package/helpers/menuHelper.js +64 -12
- package/helpers/viteHelper.js +1 -2
- package/index.d.ts +6 -2
- package/index.js +6 -2
- package/interfaces/adminpanelConfig.d.ts +126 -39
- package/interfaces/internalModelAccess.d.ts +37 -0
- package/interfaces/internalModelAccess.js +1 -0
- package/interfaces/listQueryBuilder.d.ts +22 -0
- package/interfaces/listQueryBuilder.js +1 -0
- package/interfaces/queryCriteria.d.ts +54 -0
- package/interfaces/queryCriteria.js +1 -0
- package/interfaces/types.d.ts +1 -13
- package/lib/Adminizer.d.ts +4 -2
- package/lib/Adminizer.js +29 -10
- package/lib/DataAccessor.d.ts +8 -5
- package/lib/DataAccessor.js +89 -42
- package/lib/I18n.d.ts +5 -1
- package/lib/I18n.js +45 -19
- package/lib/MiddlewareManager.d.ts +8 -0
- package/lib/MiddlewareManager.js +53 -0
- package/lib/ai-assistant/OpenAiModelService.d.ts +3 -3
- package/lib/ai-assistant/OpenAiModelService.js +23 -24
- package/lib/catalog/Navigation.d.ts +1 -0
- package/lib/catalog/Navigation.js +12 -11
- package/lib/controls/jsoneditor/JsonEditor.d.ts +1 -2
- package/lib/controls/markdown/ToastUiEditor.d.ts +1 -2
- package/lib/feedback/FeedbackHandler.js +2 -2
- package/lib/filters/ConditionValidator.d.ts +107 -0
- package/lib/filters/ConditionValidator.js +483 -0
- package/lib/filters/CustomFilterHandler.d.ts +122 -0
- package/lib/filters/CustomFilterHandler.js +131 -0
- package/lib/filters/FilterCustomFieldHandler.d.ts +2 -0
- package/lib/filters/FilterCustomFieldHandler.js +2 -0
- package/lib/filters/FilterService.d.ts +82 -0
- package/lib/filters/FilterService.js +323 -0
- package/lib/filters/index.d.ts +6 -0
- package/lib/filters/index.js +4 -0
- package/lib/history-actions/AbstractHistoryAdapter.d.ts +5 -4
- package/lib/history-actions/AbstractHistoryAdapter.js +45 -30
- package/lib/history-actions/DefaultHistoryAdapter.d.ts +2 -0
- package/lib/history-actions/DefaultHistoryAdapter.js +12 -6
- package/lib/history-actions/HistoryHandler.js +1 -1
- package/lib/list-query-builder/ListQueryBuilder.d.ts +117 -0
- package/lib/list-query-builder/ListQueryBuilder.js +707 -0
- package/lib/media-manager/AbstractMediaManager.d.ts +9 -8
- package/lib/media-manager/DefaultMediaManager.d.ts +2 -0
- package/lib/media-manager/DefaultMediaManager.js +27 -15
- package/lib/media-manager/Items.d.ts +4 -1
- package/lib/media-manager/Items.js +62 -59
- package/lib/media-manager/MediaManagerHandler.js +1 -1
- package/lib/media-manager/helpers/MediaManagerHelper.d.ts +5 -1
- package/lib/media-manager/helpers/MediaManagerHelper.js +38 -4
- package/lib/model/AbstractModel.d.ts +33 -25
- package/lib/model/AbstractModel.js +54 -2
- package/lib/model/InternalModelAccessFactory.d.ts +18 -0
- package/lib/model/InternalModelAccessFactory.js +82 -0
- package/lib/model/ModelHandler.d.ts +6 -0
- package/lib/model/ModelHandler.js +14 -0
- package/lib/model/adapter/sequelize.d.ts +32 -10
- package/lib/model/adapter/sequelize.js +409 -62
- package/lib/model/adapter/typeorm.d.ts +62 -0
- package/lib/model/adapter/typeorm.js +777 -0
- package/lib/model/internalModelAccessToken.d.ts +1 -0
- package/lib/model/internalModelAccessToken.js +1 -0
- package/lib/model/internalModelRepository.d.ts +3 -0
- package/lib/model/internalModelRepository.js +4 -0
- package/lib/notifications/AbstractNotificationService.d.ts +3 -0
- package/lib/notifications/AbstractNotificationService.js +42 -36
- package/lib/notifications/GeneralNotificationService.js +37 -40
- package/lib/notifications/SystemNotificationService.js +33 -36
- package/lib/requestProcessor.js +0 -3
- package/lib/ui-i18n/getUiTranslations.d.ts +1 -0
- package/lib/ui-i18n/getUiTranslations.js +3 -0
- package/lib/ui-i18n/uiTranslationKeys.d.ts +6 -0
- package/lib/ui-i18n/uiTranslationKeys.js +268 -0
- package/lib/widgets/widgetHandler.d.ts +7 -2
- package/lib/widgets/widgetHandler.js +98 -18
- package/models/FilterAP.d.ts +116 -0
- package/models/FilterAP.js +68 -0
- package/models/FilterColumnAP.d.ts +32 -0
- package/models/FilterColumnAP.js +21 -0
- package/models/GroupAP.d.ts +1 -1
- package/models/HistoryActionsAP.d.ts +3 -3
- package/models/MediaManagerMetaAP.d.ts +1 -1
- package/models/NavigationAP.d.ts +1 -1
- package/models/UserAP.d.ts +6 -1
- package/models/UserAP.js +4 -0
- package/package.json +39 -32
- package/policies/authPolicies.d.ts +12 -0
- package/policies/authPolicies.js +104 -0
- package/policies/permissionResolvers.d.ts +12 -0
- package/policies/permissionResolvers.js +52 -0
- package/services/FeedService.d.ts +70 -0
- package/services/FeedService.js +334 -0
- package/system/Router.d.ts +1 -0
- package/system/Router.js +136 -59
- package/system/bindAccessRights.js +11 -30
- package/system/bindAuthorization.js +18 -18
- package/system/bindCors.js +16 -1
- package/system/bindCustomFilterHandlers.d.ts +2 -0
- package/system/bindCustomFilterHandlers.js +4 -0
- package/system/bindInertia.js +18 -61
- package/system/bindReqFunctions.js +10 -2
- package/system/bindTranslations.js +7 -3
- package/system/buildInternalModelAccess.d.ts +4 -0
- package/system/buildInternalModelAccess.js +59 -0
- package/system/defaults.js +18 -14
- package/system/routePolicies.d.ts +2 -0
- package/system/routePolicies.js +29 -0
- package/translations/en.json +151 -39
- package/translations/ru.json +211 -64
- package/ui/components/handsontable.js +13 -27
- package/ui/components/ui/dialog-stack.js +1 -3
- package/ui/components/ui/popover.js +1 -1
- package/ui/components/ui/sidebar.js +1 -1
- package/assets/RelationDialogStack-D9UVEF9s.js +0 -407
- package/assets/RelationDialogStack-DV8OITPR.css +0 -4
- package/assets/add-BlvzWQKF.js +0 -1
- package/assets/add-group-1Uoq9NWe.js +0 -1
- package/assets/add-group-form-B01gwDGu.js +0 -1
- package/assets/add-user-BQCAsaRy.js +0 -1
- package/assets/add-user-form-Cgwc9JkI.js +0 -1
- package/assets/app-DaOMy1nl.css +0 -72
- package/assets/app-layout-CAm60qoE.js +0 -1
- package/assets/app-shell-QWCtn9ye.js +0 -1
- package/assets/app-sidebar-CfaYbnSy.js +0 -1
- package/assets/app-sidebar-header-BsPFDhZ8.js +0 -1
- package/assets/breadcrumbs-CZZirDj8.js +0 -1
- package/assets/catalog-k5myk7fk.js +0 -6
- package/assets/dashboard-BkL9ybBw.js +0 -3
- package/assets/date-picker-B5AZ_7Q9.js +0 -1
- package/assets/del-modal-BoyXZloG.js +0 -1
- package/assets/dynamic-controls-k61T9W2P.js +0 -1
- package/assets/form-1WGY0YgG.js +0 -1
- package/assets/form-DJapG3qF.js +0 -1
- package/assets/geo-json-DsMUHo0l.js +0 -4
- package/assets/handsontable-PxYdjN8c.js +0 -298
- package/assets/history-CluyR-yJ.js +0 -1
- package/assets/home-Dug9zBzM.js +0 -2
- package/assets/icon-DfDLMG5f.js +0 -1
- package/assets/init-user-CF80MEnT.js +0 -1
- package/assets/input-error-DuuqFgZa.js +0 -1
- package/assets/jsoneditor-De1FvcYu.js +0 -3882
- package/assets/list-CuexshNt.js +0 -1
- package/assets/list-table-BsEWjTKY.js +0 -22
- package/assets/login-eDemYzN-.js +0 -1
- package/assets/material-icon-DMT8KdmE.js +0 -1
- package/assets/media-manager-DfYa_mT_.js +0 -14
- package/assets/module-Cd4Tm6uq.js +0 -1
- package/assets/monaco-BhBYtg3d.js +0 -11
- package/assets/nav-footer-D3olYxtz.js +0 -1
- package/assets/nav-main-BctqyzeV.js +0 -1
- package/assets/nav-user-Bn1Lt-BZ.js +0 -1
- package/assets/notification-D64qvni-.js +0 -27
- package/assets/pagination-render-DxD78ahJ.js +0 -1
- package/assets/register-349PwYbe.js +0 -1
- package/assets/theme-switcher-DfExsHW2.js +0 -1
- package/assets/toast-editor-ESK5iWPx.js +0 -845
- package/assets/user-info-DtBK5Kh8.js +0 -1
- package/assets/user-menu-content-0XYOCMaA.js +0 -1
- package/assets/welcome-PDp-gUei.js +0 -1
- package/controllers/form.d.ts +0 -1
- package/controllers/form.js +0 -86
- package/helpers/formHelper.d.ts +0 -6
- package/helpers/formHelper.js +0 -38
- package/helpers/inertiaFromHelper.d.ts +0 -11
- package/helpers/inertiaFromHelper.js +0 -98
- package/helpers/runtimeMode.d.ts +0 -1
- package/helpers/runtimeMode.js +0 -3
- package/lib/PolicyManager.d.ts +0 -8
- package/lib/PolicyManager.js +0 -53
- package/lib/datatable/NodeTable.d.ts +0 -49
- package/lib/datatable/NodeTable.js +0 -207
- package/lib/model/adapter/waterline.d.ts +0 -32
- package/lib/model/adapter/waterline.js +0 -180
- package/migrations/index.d.ts +0 -3
- package/migrations/index.js +0 -4
- package/migrations/knex/20250106000001_adminizer_init.d.ts +0 -8
- package/migrations/knex/20250106000001_adminizer_init.js +0 -164
- package/migrations/umzug/0001.d.ts +0 -7
- package/migrations/umzug/0001.js +0 -121
- package/migrations/umzugExports.d.ts +0 -2
- package/migrations/umzugExports.js +0 -9
- package/system/bindForms.d.ts +0 -2
- package/system/bindForms.js +0 -20
- /package/{policies → middlewares}/addMessages.d.ts +0 -0
- /package/{policies → middlewares}/addMessages.js +0 -0
- /package/{policies → middlewares}/checkAuth.d.ts +0 -0
- /package/{policies → middlewares}/checkAuth.js +0 -0
- /package/{policies → middlewares}/setLocale.d.ts +0 -0
- /package/{policies → middlewares}/setLocale.js +0 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security limits for filter conditions
|
|
3
|
+
*/
|
|
4
|
+
export const FILTER_SECURITY_LIMITS = {
|
|
5
|
+
MAX_DEPTH: 10, // Maximum nesting depth
|
|
6
|
+
MAX_IN_VALUES: 1000, // Maximum elements in IN operator
|
|
7
|
+
MAX_CONDITIONS_PER_GROUP: 100, // Maximum conditions in one group
|
|
8
|
+
MAX_TOTAL_CONDITIONS: 500, // Maximum conditions in entire filter tree
|
|
9
|
+
MAX_STRING_LENGTH: 10000 // Maximum string length in filter value
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* ListQueryBuilder - replaces legacy NodeTable
|
|
13
|
+
*
|
|
14
|
+
* Key improvements:
|
|
15
|
+
* - Promise-based API (no callbacks)
|
|
16
|
+
* - Clean interface without DataTables.js format
|
|
17
|
+
* - Direct integration with FilterCondition
|
|
18
|
+
* - Support for CustomFilterHandler
|
|
19
|
+
* - TypeScript type safety
|
|
20
|
+
* - Full operator support (eq, neq, gt, gte, lt, lte, like, ilike, in, between, isNull, regex, custom)
|
|
21
|
+
* - Nested AND/OR/NOT groups
|
|
22
|
+
*/
|
|
23
|
+
export class ListQueryBuilder {
|
|
24
|
+
model;
|
|
25
|
+
fields;
|
|
26
|
+
dataAccessor;
|
|
27
|
+
customFilterHandler;
|
|
28
|
+
fieldsArray = ['actions'];
|
|
29
|
+
constructor(model, fields, dataAccessor, customFilterHandler) {
|
|
30
|
+
this.model = model;
|
|
31
|
+
this.fields = fields;
|
|
32
|
+
this.dataAccessor = dataAccessor;
|
|
33
|
+
this.customFilterHandler = customFilterHandler;
|
|
34
|
+
this.fieldsArray = this.fieldsArray.concat(Object.keys(this.fields));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Public method to build WHERE clause without executing query
|
|
38
|
+
* Useful for external execution (e.g., direct adapter calls)
|
|
39
|
+
*/
|
|
40
|
+
buildWhereClause(params) {
|
|
41
|
+
return this.buildWhere(params);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Execute query with Promise API
|
|
45
|
+
*/
|
|
46
|
+
async execute(params) {
|
|
47
|
+
const whereClause = this.buildWhere(params);
|
|
48
|
+
const orderClause = this.buildOrder(params);
|
|
49
|
+
const offset = (params.page - 1) * params.limit;
|
|
50
|
+
// Execute queries in parallel
|
|
51
|
+
const [data, total, filtered] = await Promise.all([
|
|
52
|
+
this.model.find({
|
|
53
|
+
where: whereClause,
|
|
54
|
+
sort: orderClause,
|
|
55
|
+
limit: params.limit,
|
|
56
|
+
skip: offset,
|
|
57
|
+
select: this.buildSelect(params.fields)
|
|
58
|
+
}, this.dataAccessor),
|
|
59
|
+
this.model.count({}, this.dataAccessor),
|
|
60
|
+
this.model.count({ where: whereClause }, this.dataAccessor)
|
|
61
|
+
]);
|
|
62
|
+
return {
|
|
63
|
+
data: this.mapData(data),
|
|
64
|
+
total,
|
|
65
|
+
filtered,
|
|
66
|
+
page: params.page,
|
|
67
|
+
limit: params.limit,
|
|
68
|
+
pages: Math.ceil(filtered / params.limit)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build WHERE clause from FilterCondition[]
|
|
73
|
+
* Supports nested AND/OR/NOT groups
|
|
74
|
+
*/
|
|
75
|
+
buildWhere(params) {
|
|
76
|
+
const conditions = [];
|
|
77
|
+
// 1. Filters from FilterCondition
|
|
78
|
+
if (params.filters && params.filters.length > 0) {
|
|
79
|
+
const filterCondition = this.buildConditionGroup(params.filters, 'AND');
|
|
80
|
+
// Check if filterCondition is not empty (including null values)
|
|
81
|
+
const hasKeys = Object.keys(filterCondition).length > 0;
|
|
82
|
+
const hasNullValue = Object.values(filterCondition).some(v => v === null);
|
|
83
|
+
if (hasKeys || hasNullValue) {
|
|
84
|
+
conditions.push(filterCondition);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 2. Global search (for compatibility)
|
|
88
|
+
if (params.globalSearch) {
|
|
89
|
+
const searchConditions = this.buildGlobalSearch(params.globalSearch);
|
|
90
|
+
if (searchConditions && Object.keys(searchConditions).length > 0) {
|
|
91
|
+
conditions.push(searchConditions);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Combine with AND
|
|
95
|
+
if (conditions.length === 0) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
if (conditions.length === 1) {
|
|
99
|
+
return conditions[0];
|
|
100
|
+
}
|
|
101
|
+
return { and: conditions };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Recursive condition group builder
|
|
105
|
+
* Key method for complex filter support
|
|
106
|
+
*/
|
|
107
|
+
buildConditionGroup(conditions, logic = 'AND', depth = 0) {
|
|
108
|
+
// Security: check depth
|
|
109
|
+
if (depth > FILTER_SECURITY_LIMITS.MAX_DEPTH) {
|
|
110
|
+
throw new Error(`Filter nesting too deep (max ${FILTER_SECURITY_LIMITS.MAX_DEPTH})`);
|
|
111
|
+
}
|
|
112
|
+
// Security: check conditions count
|
|
113
|
+
if (conditions.length > FILTER_SECURITY_LIMITS.MAX_CONDITIONS_PER_GROUP) {
|
|
114
|
+
throw new Error(`Too many conditions in group (max ${FILTER_SECURITY_LIMITS.MAX_CONDITIONS_PER_GROUP})`);
|
|
115
|
+
}
|
|
116
|
+
const clauses = conditions
|
|
117
|
+
.filter(cond => this.isValidCondition(cond, depth))
|
|
118
|
+
.map(cond => {
|
|
119
|
+
// Recursion for nested groups
|
|
120
|
+
if (cond.children && cond.children.length > 0) {
|
|
121
|
+
return this.buildConditionGroup(cond.children, cond.logic || 'AND', depth + 1);
|
|
122
|
+
}
|
|
123
|
+
// Simple condition
|
|
124
|
+
return this.buildSingleCondition(cond);
|
|
125
|
+
})
|
|
126
|
+
.filter(c => {
|
|
127
|
+
if (!c)
|
|
128
|
+
return false;
|
|
129
|
+
// Check for regular keys
|
|
130
|
+
const keys = Object.keys(c);
|
|
131
|
+
if (keys.length > 0) {
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
const val = c[key];
|
|
134
|
+
// null is a valid value (IS NULL condition)
|
|
135
|
+
if (val === null)
|
|
136
|
+
return true;
|
|
137
|
+
if (val && typeof val === 'object') {
|
|
138
|
+
if (Object.keys(val).length > 0)
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
else if (val !== undefined) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
});
|
|
148
|
+
if (clauses.length === 0) {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
if (clauses.length === 1) {
|
|
152
|
+
// NOT operator
|
|
153
|
+
if (logic === 'NOT') {
|
|
154
|
+
return { not: clauses[0] };
|
|
155
|
+
}
|
|
156
|
+
return clauses[0];
|
|
157
|
+
}
|
|
158
|
+
// NOT operator requires exactly one condition
|
|
159
|
+
if (logic === 'NOT') {
|
|
160
|
+
throw new Error('NOT operator requires exactly one condition');
|
|
161
|
+
}
|
|
162
|
+
return logic === 'OR'
|
|
163
|
+
? { or: clauses }
|
|
164
|
+
: { and: clauses };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Validate condition
|
|
168
|
+
*/
|
|
169
|
+
isValidCondition(cond, _depth = 0) {
|
|
170
|
+
// Group with children
|
|
171
|
+
if (cond.children && cond.children.length > 0) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
const hasRelation = Boolean(cond.relation && cond.relationField);
|
|
175
|
+
// Regular condition
|
|
176
|
+
if (!cond.field && !hasRelation && !cond.customHandler && !cond.rawSQL) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (!cond.operator) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// Operators without value
|
|
183
|
+
if (cond.operator === 'isNull' || cond.operator === 'isNotNull' || cond.operator === 'today') {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
// Custom handlers may have their own validation
|
|
187
|
+
if (cond.customHandler || cond.rawSQL) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
// Relation conditions must have value (except NULL checks handled above)
|
|
191
|
+
if (hasRelation) {
|
|
192
|
+
return cond.value !== undefined && cond.value !== '';
|
|
193
|
+
}
|
|
194
|
+
// Others require value
|
|
195
|
+
return cond.value !== undefined && cond.value !== '';
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build single condition
|
|
199
|
+
*/
|
|
200
|
+
buildSingleCondition(cond) {
|
|
201
|
+
// 1. Raw SQL is intentionally not part of QueryCriteria.
|
|
202
|
+
if (cond.rawSQL) {
|
|
203
|
+
throw new Error("Raw SQL filters are not supported by QueryCriteria");
|
|
204
|
+
}
|
|
205
|
+
// 2. Custom handler
|
|
206
|
+
if (cond.customHandler) {
|
|
207
|
+
return this.handleCustomCondition(cond);
|
|
208
|
+
}
|
|
209
|
+
// 3. Relation condition
|
|
210
|
+
if (cond.relation && cond.relationField) {
|
|
211
|
+
return this.buildRelationCondition(cond);
|
|
212
|
+
}
|
|
213
|
+
// 4. Standard field condition
|
|
214
|
+
const { field, operator, value } = cond;
|
|
215
|
+
// Ensure field and operator exist
|
|
216
|
+
if (!field || !operator) {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
// Get field config to handle special cases
|
|
220
|
+
const fieldConfig = this.fields[field];
|
|
221
|
+
const fieldType = fieldConfig?.model?.type;
|
|
222
|
+
const fieldConfigType = typeof fieldConfig?.config === 'object' && fieldConfig?.config !== null
|
|
223
|
+
? fieldConfig.config?.type
|
|
224
|
+
: undefined;
|
|
225
|
+
// Special handling for select-many (JSON array stored in DB) with in/notIn/eq operators
|
|
226
|
+
// select-many widget stores data as JSON array: ["value1","value2"]
|
|
227
|
+
if ((fieldType === 'json' || fieldConfigType === 'select-many') &&
|
|
228
|
+
Array.isArray(value)) {
|
|
229
|
+
if (operator === 'eq') {
|
|
230
|
+
return { [field]: { jsonContains: value } };
|
|
231
|
+
}
|
|
232
|
+
if (operator === 'in' || operator === 'notIn') {
|
|
233
|
+
const clauses = value.map((item) => ({ [field]: { jsonContains: item } }));
|
|
234
|
+
return operator === 'in'
|
|
235
|
+
? { or: clauses }
|
|
236
|
+
: { not: { or: clauses } };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Special handling for isNull - return { field: null }
|
|
240
|
+
if (operator === 'isNull') {
|
|
241
|
+
return { [field]: null };
|
|
242
|
+
}
|
|
243
|
+
// Special handling for isNotNull - return { not: { field: null } }
|
|
244
|
+
if (operator === 'isNotNull') {
|
|
245
|
+
return { [field]: { isNotNull: true } };
|
|
246
|
+
}
|
|
247
|
+
// Validate operator-value combination
|
|
248
|
+
this.validateOperatorValue(operator, value);
|
|
249
|
+
// Map operator to ORM condition (ignore value for isNull/isNotNull)
|
|
250
|
+
const condition = this.mapOperatorToCondition(operator, value);
|
|
251
|
+
// Skip if condition is undefined/null (invalid value)
|
|
252
|
+
if (condition === undefined || condition === null) {
|
|
253
|
+
return {};
|
|
254
|
+
}
|
|
255
|
+
return { [field]: condition };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Map filter operators to adapter-neutral criteria operators.
|
|
259
|
+
*/
|
|
260
|
+
mapOperatorToCondition(operator, value) {
|
|
261
|
+
const operatorsWithoutValue = ['isNull', 'isNotNull', 'today'];
|
|
262
|
+
// Return undefined for invalid values (will be filtered out)
|
|
263
|
+
// Note: false is a valid boolean value and should NOT be filtered out
|
|
264
|
+
if (!operatorsWithoutValue.includes(operator) && (value === undefined || value === null || value === '')) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
switch (operator) {
|
|
268
|
+
case 'eq':
|
|
269
|
+
// Convert string 'true'/'false' to actual boolean values
|
|
270
|
+
// For boolean fields, return value directly.
|
|
271
|
+
if (value === 'true')
|
|
272
|
+
return true;
|
|
273
|
+
if (value === 'false')
|
|
274
|
+
return false;
|
|
275
|
+
if (typeof value === 'boolean')
|
|
276
|
+
return value;
|
|
277
|
+
return value;
|
|
278
|
+
case 'neq':
|
|
279
|
+
// Convert string 'true'/'false' to actual boolean values
|
|
280
|
+
const neqValue = value === 'true' ? true : value === 'false' ? false : value;
|
|
281
|
+
return { ne: neqValue };
|
|
282
|
+
case 'gt':
|
|
283
|
+
return { gt: value };
|
|
284
|
+
case 'gte':
|
|
285
|
+
return { gte: value };
|
|
286
|
+
case 'lt':
|
|
287
|
+
return { lt: value };
|
|
288
|
+
case 'lte':
|
|
289
|
+
return { lte: value };
|
|
290
|
+
case 'like':
|
|
291
|
+
if (typeof value === 'string' && value.startsWith('%') && value.endsWith('%')) {
|
|
292
|
+
return { contains: value.slice(1, -1) };
|
|
293
|
+
}
|
|
294
|
+
return { contains: value };
|
|
295
|
+
case 'ilike':
|
|
296
|
+
// Case-insensitive LIKE - works on all dialects
|
|
297
|
+
if (typeof value === 'string' && value.startsWith('%') && value.endsWith('%')) {
|
|
298
|
+
return { contains: value.slice(1, -1) };
|
|
299
|
+
}
|
|
300
|
+
return { contains: value };
|
|
301
|
+
case 'startsWith':
|
|
302
|
+
return { startsWith: value };
|
|
303
|
+
case 'endsWith':
|
|
304
|
+
return { endsWith: value };
|
|
305
|
+
case 'regex':
|
|
306
|
+
return { regex: value };
|
|
307
|
+
case 'in':
|
|
308
|
+
// Convert string 'true'/'false' to actual boolean values in array
|
|
309
|
+
const inValues = Array.isArray(value) ? value : [value];
|
|
310
|
+
const convertedInValues = inValues.map((v) => v === 'true' ? true : v === 'false' ? false : v);
|
|
311
|
+
return { in: convertedInValues };
|
|
312
|
+
case 'notIn':
|
|
313
|
+
// Convert string 'true'/'false' to actual boolean values in array
|
|
314
|
+
const notInValues = Array.isArray(value) ? value : [value];
|
|
315
|
+
const convertedNotInValues = notInValues.map((v) => v === 'true' ? true : v === 'false' ? false : v);
|
|
316
|
+
return { notIn: convertedNotInValues };
|
|
317
|
+
case 'between':
|
|
318
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
319
|
+
return { between: value };
|
|
320
|
+
}
|
|
321
|
+
return value;
|
|
322
|
+
case 'today': {
|
|
323
|
+
const [from, to] = this.getDayRange(new Date());
|
|
324
|
+
return { between: [from, to] };
|
|
325
|
+
}
|
|
326
|
+
case 'month': {
|
|
327
|
+
const range = this.getMonthRangeByValue(value);
|
|
328
|
+
return range ? { between: range } : undefined;
|
|
329
|
+
}
|
|
330
|
+
case 'year': {
|
|
331
|
+
const range = this.getYearRangeByValue(value);
|
|
332
|
+
return range ? { between: range } : undefined;
|
|
333
|
+
}
|
|
334
|
+
case 'monthBetween': {
|
|
335
|
+
const range = this.getMonthRangeBetween(value);
|
|
336
|
+
return range ? { between: range } : undefined;
|
|
337
|
+
}
|
|
338
|
+
case 'yearBetween': {
|
|
339
|
+
const range = this.getYearRangeBetween(value);
|
|
340
|
+
return range ? { between: range } : undefined;
|
|
341
|
+
}
|
|
342
|
+
case 'custom':
|
|
343
|
+
// Custom conditions are handled separately
|
|
344
|
+
return value;
|
|
345
|
+
default:
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Validate operator-value combination
|
|
351
|
+
*/
|
|
352
|
+
validateOperatorValue(operator, value) {
|
|
353
|
+
const operatorsWithoutValue = ['isNull', 'isNotNull', 'today'];
|
|
354
|
+
// Skip validation for undefined/null/empty values (will be filtered out)
|
|
355
|
+
if (!operatorsWithoutValue.includes(operator) && (value === undefined || value === null || value === '')) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
switch (operator) {
|
|
359
|
+
case 'in':
|
|
360
|
+
case 'notIn':
|
|
361
|
+
if (!Array.isArray(value)) {
|
|
362
|
+
throw new Error(`Operator '${operator}' requires array value`);
|
|
363
|
+
}
|
|
364
|
+
if (value.length > FILTER_SECURITY_LIMITS.MAX_IN_VALUES) {
|
|
365
|
+
throw new Error(`Too many values in IN operator (max ${FILTER_SECURITY_LIMITS.MAX_IN_VALUES})`);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
case 'between':
|
|
369
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
370
|
+
throw new Error('BETWEEN operator requires array of 2 values');
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
case 'month':
|
|
374
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}$/.test(value)) {
|
|
375
|
+
throw new Error('MONTH operator requires YYYY-MM value');
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
case 'year':
|
|
379
|
+
if (!((typeof value === 'string' && /^\d{4}$/.test(value)) ||
|
|
380
|
+
(typeof value === 'number' && Number.isInteger(value)))) {
|
|
381
|
+
throw new Error('YEAR operator requires YYYY value');
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
case 'monthBetween':
|
|
385
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
386
|
+
throw new Error('MONTH BETWEEN operator requires array of 2 YYYY-MM values');
|
|
387
|
+
}
|
|
388
|
+
if (typeof value[0] !== 'string' || !/^\d{4}-\d{2}$/.test(value[0]) ||
|
|
389
|
+
typeof value[1] !== 'string' || !/^\d{4}-\d{2}$/.test(value[1])) {
|
|
390
|
+
throw new Error('MONTH BETWEEN operator requires array of 2 YYYY-MM values');
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
case 'yearBetween':
|
|
394
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
395
|
+
throw new Error('YEAR BETWEEN operator requires array of 2 YYYY values');
|
|
396
|
+
}
|
|
397
|
+
if (!this.isYearValue(value[0]) || !this.isYearValue(value[1])) {
|
|
398
|
+
throw new Error('YEAR BETWEEN operator requires array of 2 YYYY values');
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
case 'regex':
|
|
402
|
+
if (typeof value !== 'string') {
|
|
403
|
+
throw new Error('Regex operator requires string pattern');
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
new RegExp(value);
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
throw new Error(`Invalid regex pattern: ${value}`);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
case 'like':
|
|
413
|
+
case 'ilike':
|
|
414
|
+
case 'startsWith':
|
|
415
|
+
case 'endsWith':
|
|
416
|
+
if (typeof value === 'string' && value.length > FILTER_SECURITY_LIMITS.MAX_STRING_LENGTH) {
|
|
417
|
+
throw new Error(`String value too long (max ${FILTER_SECURITY_LIMITS.MAX_STRING_LENGTH})`);
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
getDayRange(date) {
|
|
423
|
+
const from = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
|
424
|
+
const to = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
|
425
|
+
return [from, to];
|
|
426
|
+
}
|
|
427
|
+
getMonthRangeByValue(value) {
|
|
428
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}$/.test(value)) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const [year, month] = value.split('-').map(Number);
|
|
432
|
+
if (!year || !month || month < 1 || month > 12) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const from = new Date(year, month - 1, 1, 0, 0, 0, 0);
|
|
436
|
+
const to = new Date(year, month, 0, 23, 59, 59, 999);
|
|
437
|
+
return [from, to];
|
|
438
|
+
}
|
|
439
|
+
getYearRangeByValue(value) {
|
|
440
|
+
const year = this.parseYearValue(value);
|
|
441
|
+
if (year === null) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const from = new Date(year, 0, 1, 0, 0, 0, 0);
|
|
445
|
+
const to = new Date(year, 11, 31, 23, 59, 59, 999);
|
|
446
|
+
return [from, to];
|
|
447
|
+
}
|
|
448
|
+
getMonthRangeBetween(value) {
|
|
449
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const fromRange = this.getMonthRangeByValue(value[0]);
|
|
453
|
+
const toRange = this.getMonthRangeByValue(value[1]);
|
|
454
|
+
if (!fromRange || !toRange) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const start = fromRange[0] <= toRange[0] ? fromRange[0] : toRange[0];
|
|
458
|
+
const end = fromRange[1] <= toRange[1] ? toRange[1] : fromRange[1];
|
|
459
|
+
return [start, end];
|
|
460
|
+
}
|
|
461
|
+
getYearRangeBetween(value) {
|
|
462
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const fromRange = this.getYearRangeByValue(value[0]);
|
|
466
|
+
const toRange = this.getYearRangeByValue(value[1]);
|
|
467
|
+
if (!fromRange || !toRange) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const start = fromRange[0] <= toRange[0] ? fromRange[0] : toRange[0];
|
|
471
|
+
const end = fromRange[1] <= toRange[1] ? toRange[1] : fromRange[1];
|
|
472
|
+
return [start, end];
|
|
473
|
+
}
|
|
474
|
+
isYearValue(value) {
|
|
475
|
+
return this.parseYearValue(value) !== null;
|
|
476
|
+
}
|
|
477
|
+
parseYearValue(value) {
|
|
478
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
if (typeof value === 'string' && /^\d{4}$/.test(value)) {
|
|
482
|
+
return Number(value);
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Build relation condition
|
|
488
|
+
*/
|
|
489
|
+
buildRelationCondition(cond) {
|
|
490
|
+
const { relation, relationField, operator, value } = cond;
|
|
491
|
+
// Ensure operator and safe relation path parts exist
|
|
492
|
+
if (!relation || !relationField || !operator) {
|
|
493
|
+
return {};
|
|
494
|
+
}
|
|
495
|
+
if (!/^[A-Za-z0-9_]+$/.test(relation) || !/^[A-Za-z0-9_]+$/.test(relationField)) {
|
|
496
|
+
return {};
|
|
497
|
+
}
|
|
498
|
+
const relationPath = `$${relation}.${relationField}$`;
|
|
499
|
+
if (operator === 'isNull') {
|
|
500
|
+
return { [relationPath]: null };
|
|
501
|
+
}
|
|
502
|
+
if (operator === 'isNotNull') {
|
|
503
|
+
return { [relationPath]: { isNotNull: true } };
|
|
504
|
+
}
|
|
505
|
+
this.validateOperatorValue(operator, value);
|
|
506
|
+
const relationCondition = this.mapOperatorToCondition(operator, value);
|
|
507
|
+
if (relationCondition === undefined || relationCondition === null) {
|
|
508
|
+
return {};
|
|
509
|
+
}
|
|
510
|
+
return { [relationPath]: relationCondition };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Handle custom condition
|
|
514
|
+
*/
|
|
515
|
+
handleCustomCondition(cond) {
|
|
516
|
+
const handler = this.customFilterHandler?.get(cond.customHandler || '');
|
|
517
|
+
if (handler) {
|
|
518
|
+
const condition = handler.buildCondition(cond.value, cond.customHandlerParams);
|
|
519
|
+
// rawSQL returned
|
|
520
|
+
if (condition.rawSQL) {
|
|
521
|
+
throw new Error("Raw SQL filters are not supported by QueryCriteria");
|
|
522
|
+
}
|
|
523
|
+
// in-memory function returned
|
|
524
|
+
if (condition.inMemory) {
|
|
525
|
+
throw new Error("In-memory filters are not supported by QueryCriteria");
|
|
526
|
+
}
|
|
527
|
+
// Standard criteria returned
|
|
528
|
+
if (condition.criteria) {
|
|
529
|
+
return condition.criteria;
|
|
530
|
+
}
|
|
531
|
+
return condition;
|
|
532
|
+
}
|
|
533
|
+
return {};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Build global search condition
|
|
537
|
+
* Searches in string/text/ref fields (same behavior as NodeTable)
|
|
538
|
+
* Date/time fields are excluded from global search to avoid moment.js warnings
|
|
539
|
+
*/
|
|
540
|
+
buildGlobalSearch(searchStr) {
|
|
541
|
+
// Skip if empty
|
|
542
|
+
if (!searchStr || searchStr.trim() === '') {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const searchConditions = [];
|
|
546
|
+
for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
|
|
547
|
+
// Skip non-visible fields (same behavior as NodeTable)
|
|
548
|
+
const config = typeof fieldConfig.config === 'object' && fieldConfig.config !== null
|
|
549
|
+
? fieldConfig.config
|
|
550
|
+
: null;
|
|
551
|
+
if (config && config.visible === false) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const fieldType = fieldConfig.model?.type;
|
|
555
|
+
// Skip date/datetime fields - they can't be searched with LIKE
|
|
556
|
+
// This avoids moment.js warnings when string values are passed to date fields
|
|
557
|
+
if (fieldType === 'date' || fieldType === 'datetime') {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
// Global search in string/text/ref fields
|
|
561
|
+
if (fieldType === 'string' || fieldType === 'text' || fieldType === 'ref') {
|
|
562
|
+
searchConditions.push({
|
|
563
|
+
[fieldName]: { contains: searchStr }
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (searchConditions.length === 0) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
if (searchConditions.length === 1) {
|
|
571
|
+
return searchConditions[0];
|
|
572
|
+
}
|
|
573
|
+
return { or: searchConditions };
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Build ORDER clause
|
|
577
|
+
*/
|
|
578
|
+
buildOrder(params) {
|
|
579
|
+
const sortField = params.sort || 'createdAt';
|
|
580
|
+
const sortDirection = this.sanitizeSortDirection(params.sortDirection);
|
|
581
|
+
// Validate sort field exists
|
|
582
|
+
if (sortField !== 'createdAt' && sortField !== 'updatedAt') {
|
|
583
|
+
const fieldExists = sortField in this.fields ||
|
|
584
|
+
sortField === this.model.primaryKey ||
|
|
585
|
+
sortField === 'id';
|
|
586
|
+
if (!fieldExists) {
|
|
587
|
+
// Fallback to createdAt if field doesn't exist
|
|
588
|
+
return 'createdAt DESC';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return `${sortField} ${sortDirection}`;
|
|
592
|
+
}
|
|
593
|
+
sanitizeSortDirection(sortDirection) {
|
|
594
|
+
if (!sortDirection) {
|
|
595
|
+
return 'DESC';
|
|
596
|
+
}
|
|
597
|
+
const normalizedDirection = sortDirection.toUpperCase();
|
|
598
|
+
if (normalizedDirection !== 'ASC' && normalizedDirection !== 'DESC') {
|
|
599
|
+
return 'DESC';
|
|
600
|
+
}
|
|
601
|
+
return normalizedDirection;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Map data for output
|
|
605
|
+
*/
|
|
606
|
+
mapData(data) {
|
|
607
|
+
const out = [];
|
|
608
|
+
data.forEach((elem) => {
|
|
609
|
+
const row = {};
|
|
610
|
+
// Add primary key first
|
|
611
|
+
const pk = this.model.primaryKey ?? 'id';
|
|
612
|
+
row[pk] = elem[pk];
|
|
613
|
+
// Process each field
|
|
614
|
+
Object.keys(this.fields).forEach((key) => {
|
|
615
|
+
const fieldConfig = this.fields[key].config;
|
|
616
|
+
const fieldModel = this.fields[key].model;
|
|
617
|
+
const modelConfig = this.fields[key].modelConfig;
|
|
618
|
+
const displayField = modelConfig?.titleField ??
|
|
619
|
+
(typeof elem['title'] !== 'undefined' ? 'title' :
|
|
620
|
+
typeof elem['name'] !== 'undefined' ? 'name' :
|
|
621
|
+
modelConfig?.identifierField ?? 'id');
|
|
622
|
+
if (fieldConfig.displayModifier) {
|
|
623
|
+
// Custom display modifier
|
|
624
|
+
row[key] = fieldConfig.displayModifier(elem[key]);
|
|
625
|
+
}
|
|
626
|
+
else if (fieldModel && fieldModel.model) {
|
|
627
|
+
// Handle "belongsTo" relationships
|
|
628
|
+
if (!elem[key]) {
|
|
629
|
+
row[key] = null;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
row[key] = elem[key][displayField];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else if (fieldModel?.type === 'association-many' || fieldModel?.type === 'association') {
|
|
636
|
+
// Handle "hasMany" relationships
|
|
637
|
+
if (!elem[key] || elem[key].length === 0) {
|
|
638
|
+
row[key] = null;
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const displayValues = [];
|
|
642
|
+
elem[key].forEach((item) => {
|
|
643
|
+
if (item[displayField]) {
|
|
644
|
+
displayValues.push(item[displayField]);
|
|
645
|
+
}
|
|
646
|
+
else if (fieldConfig.identifierField && item[fieldConfig.identifierField]) {
|
|
647
|
+
displayValues.push(item[fieldConfig.identifierField]);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
row[key] = displayValues.join(', ');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else if (fieldModel?.type === 'json') {
|
|
654
|
+
// Handle JSON fields
|
|
655
|
+
const str = JSON.stringify(elem[key]);
|
|
656
|
+
row[key] = str === '{}' ? '' : str;
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
// Regular field
|
|
660
|
+
row[key] = elem[key];
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
out.push(row);
|
|
664
|
+
});
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
buildSelect(fields) {
|
|
668
|
+
if (!fields?.length) {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
const selectableFields = fields.filter((field) => {
|
|
672
|
+
const modelField = this.model.attributes[field];
|
|
673
|
+
if (!modelField) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
return !modelField.model
|
|
677
|
+
&& !modelField.collection
|
|
678
|
+
&& modelField.type !== "association"
|
|
679
|
+
&& modelField.type !== "association-many";
|
|
680
|
+
});
|
|
681
|
+
if (this.model.primaryKey && !selectableFields.includes(this.model.primaryKey)) {
|
|
682
|
+
selectableFields.unshift(this.model.primaryKey);
|
|
683
|
+
}
|
|
684
|
+
return selectableFields.length ? selectableFields : undefined;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Get allowed operators for field type
|
|
688
|
+
*/
|
|
689
|
+
static getOperatorsForFieldType(fieldType) {
|
|
690
|
+
const operatorsByType = {
|
|
691
|
+
string: ['eq', 'neq', 'like', 'ilike', 'startsWith', 'endsWith', 'in', 'notIn', 'isNull', 'isNotNull', 'regex'],
|
|
692
|
+
text: ['eq', 'neq', 'like', 'ilike', 'startsWith', 'endsWith', 'isNull', 'isNotNull'],
|
|
693
|
+
number: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
|
|
694
|
+
integer: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
|
|
695
|
+
float: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],
|
|
696
|
+
boolean: ['eq', 'neq', 'isNull', 'isNotNull'],
|
|
697
|
+
date: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'today', 'month', 'year', 'monthBetween', 'yearBetween', 'isNull', 'isNotNull'],
|
|
698
|
+
datetime: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'today', 'month', 'year', 'monthBetween', 'yearBetween', 'isNull', 'isNotNull'],
|
|
699
|
+
select: ['eq', 'neq', 'in', 'notIn', 'isNull', 'isNotNull'],
|
|
700
|
+
json: ['isNull', 'isNotNull', 'custom'],
|
|
701
|
+
association: ['eq', 'neq', 'in', 'notIn', 'isNull', 'isNotNull'],
|
|
702
|
+
'association-many': ['in', 'notIn', 'isNull', 'isNotNull']
|
|
703
|
+
};
|
|
704
|
+
return operatorsByType[fieldType] || operatorsByType.string;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
export { ListQueryBuilder as ModernListQueryBuilder };
|