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.
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-4213XScp.js +1 -0
  5. package/assets/add-group--F1XxfJ1.js +1 -0
  6. package/assets/add-group-form-DKRXC2n-.js +1 -0
  7. package/assets/add-user-b9HfMq7z.js +1 -0
  8. package/assets/add-user-form-UXAToWQd.js +1 -0
  9. package/assets/app-Clx8f5hd.css +1 -0
  10. package/assets/app-layout-C02ja4OI.js +1 -0
  11. package/assets/app-shell-D8wtkECO.js +1 -0
  12. package/assets/app-sidebar-BR5_zwxj.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-6ZZjWdFs.js +6 -0
  17. package/assets/dashboard-CftAdKiH.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-DLHSU7Ak.js +1 -0
  24. package/assets/home-NcQ7nnU0.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-CFU0-TgN.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-CT7PC6pc.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-Bi-PX87D.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-YUfR4XMI.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-vLwTu1R6.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
@@ -1,8 +1,7 @@
1
1
  import { ControllerHelper } from "../helpers/controllerHelper.js";
2
2
  import { RequestProcessor } from "../lib/requestProcessor.js";
3
3
  import { FieldsHelper } from "../helpers/fieldsHelper.js";
4
- import { redirectToLogin } from '../helpers/inertiaAutHelper.js';
5
- import { detachMediaManagerField, getRelationsMediaManager, saveRelationsMediaManager } from "../lib/media-manager/helpers/MediaManagerHelper.js";
4
+ import { detachMediaManagerField, getRelationsMediaManager, isMediaManagerFieldConfig, saveRelationsMediaManager, updateCurrentHistoryMediaManagerData } from "../lib/media-manager/helpers/MediaManagerHelper.js";
6
5
  import { DataAccessor } from "../lib/DataAccessor.js";
7
6
  import { Adminizer } from "../lib/Adminizer.js";
8
7
  import inertiaAddHelper from "../helpers/inertiaAddHelper.js";
@@ -11,27 +10,19 @@ export default async function edit(req, res) {
11
10
  if (!req.params.id) {
12
11
  return res.status(404).send({ error: 'Not Found' });
13
12
  }
14
- let entity = ControllerHelper.findEntityObject(req);
15
- if (!entity.model) {
13
+ let modelResource = ControllerHelper.findModelResource(req);
14
+ if (!modelResource.model) {
16
15
  return res.status(404).send({ error: 'Not Found' });
17
16
  }
18
- if (!entity.config.edit) {
19
- return res.redirect(`${req.adminizer.config.routePrefix}/${entity.uri}`);
20
- }
21
- if (req.adminizer.config.auth.enable) {
22
- if (!req.user) {
23
- return redirectToLogin(req, res);
24
- }
25
- else if (!req.adminizer.accessRightsHelper.hasPermission(`update-${entity.name}-model`, req.user)) {
26
- return res.sendStatus(403);
27
- }
17
+ if (!modelResource.config.edit) {
18
+ return res.redirect(`${req.adminizer.config.routePrefix}/${modelResource.uri}`);
28
19
  }
29
20
  let record;
30
21
  let dataAccessor;
31
22
  const id = req.params.id;
32
23
  try {
33
- dataAccessor = new DataAccessor(req.adminizer, req.user, entity, "edit");
34
- record = await entity.model.findOne({ id: id }, dataAccessor);
24
+ dataAccessor = new DataAccessor(req.adminizer, req.user, modelResource, "edit");
25
+ record = await modelResource.model.findOne({ where: { id: id } }, dataAccessor);
35
26
  if (!record)
36
27
  return res.status(404).send("Adminpanel > Record not found");
37
28
  }
@@ -45,11 +36,11 @@ export default async function edit(req, res) {
45
36
  fields = await FieldsHelper.loadAssociations(req, fields, "edit");
46
37
  // Save
47
38
  if (req.method.toUpperCase() === 'POST') {
48
- const backUrl = req.body.redirectUrl;
39
+ const identifierField = modelResource.config.identifierField || req.adminizer.config.identifierField;
49
40
  delete req.body.redirectUrl;
50
41
  let reqData = RequestProcessor.processRequest(req, fields);
51
42
  let params = {};
52
- params[entity.config.identifierField || req.adminizer.config.identifierField] = req.params.id;
43
+ params[modelResource.config.identifierField || req.adminizer.config.identifierField] = req.params.id;
53
44
  /**
54
45
  * Here means reqData adapt for model data, but rawReqData is processed for widget processing
55
46
  */
@@ -65,8 +56,7 @@ export default async function edit(req, res) {
65
56
  reqData[prop] = null;
66
57
  }
67
58
  let fieldConfigConfig = fields[prop].config;
68
- // delete property from association-many and association if empty
69
- // TODO check if adding and deleting associations works for other orm (this code was written for waterline)
59
+ // Normalize empty association payloads before passing them to the adapter
70
60
  if (fields[prop] && fields[prop].model && (fields[prop].model.type === 'association-many' || fields[prop].model.type === 'association')) {
71
61
  if (!reqData[prop] || !reqData[prop].length) {
72
62
  reqData[prop] = fields[prop].model.type === 'association' ? null : [];
@@ -77,7 +67,7 @@ export default async function edit(req, res) {
77
67
  }
78
68
  }
79
69
  }
80
- if (fieldConfigConfig.type === 'mediamanager') {
70
+ if (isMediaManagerFieldConfig(fieldConfigConfig)) {
81
71
  detachMediaManagerField(reqData, rawReqData, prop);
82
72
  continue;
83
73
  }
@@ -102,14 +92,15 @@ export default async function edit(req, res) {
102
92
  reqData[prop] = reqData[prop].join("");
103
93
  }
104
94
  }
105
- // callback before save entity
106
- let entityEdit = entity.config.edit;
107
- if (typeof entityEdit.entityModifier === "function") {
108
- reqData = entityEdit.entityModifier(reqData);
95
+ // callback before save modelResource
96
+ let editConfig = modelResource.config.edit;
97
+ if (typeof editConfig.entityModifier === "function") {
98
+ reqData = editConfig.entityModifier(reqData);
109
99
  }
110
100
  try {
111
- let newRecord = await entity.model.update(params, reqData, dataAccessor);
112
- await saveRelationsMediaManager(req.adminizer, fields, rawReqData, entity.model.modelname, newRecord[0].id);
101
+ let newRecord = await modelResource.model.update(params, reqData, dataAccessor);
102
+ await saveRelationsMediaManager(req.adminizer, fields, rawReqData, modelResource.model.modelname, newRecord[0].id);
103
+ await updateCurrentHistoryMediaManagerData(req.adminizer, fields, rawReqData, modelResource.name, newRecord[0].id);
113
104
  Adminizer.log.debug(`Record was updated: `, newRecord);
114
105
  if (req.body.jsonPopupCatalog) {
115
106
  return res.json({ record: newRecord });
@@ -120,19 +111,15 @@ export default async function edit(req, res) {
120
111
  for (const section of req.adminizer.config.navigation.sections) {
121
112
  let navigation = req.adminizer.catalogHandler.getCatalog('navigation');
122
113
  navigation.setId(section);
123
- let navItem = navigation.itemTypes.find(item => item.type === entity.name.toLowerCase());
114
+ let navItem = navigation.itemTypes.find(item => item.type === modelResource.name.toLowerCase());
124
115
  if (navItem) {
125
116
  await navItem.updateModelItems(newRecord[0].id, { record: newRecord[0] }, section);
126
117
  }
127
118
  }
128
119
  }
129
120
  req.flash.setFlashMessage('success', req.i18n.__('Record was updated'));
130
- if (backUrl) {
131
- return req.Inertia.redirect(backUrl);
132
- }
133
- else {
134
- return req.Inertia.redirect(`${req.adminizer.config.routePrefix}/model/${entity.name}`);
135
- }
121
+ const redirectId = newRecord[0]?.[identifierField] ?? req.params.id;
122
+ return req.Inertia.redirect(`${req.adminizer.config.routePrefix}/model/${modelResource.name}/edit/${redirectId}`);
136
123
  }
137
124
  }
138
125
  catch (e) {
@@ -143,16 +130,16 @@ export default async function edit(req, res) {
143
130
  } // END POST
144
131
  for (const field of Object.keys(fields)) {
145
132
  const fieldConfigConfig = fields[field].config;
146
- if (fieldConfigConfig.type === 'mediamanager') {
133
+ if (isMediaManagerFieldConfig(fieldConfigConfig)) {
147
134
  record[field] = await getRelationsMediaManager(req.adminizer, {
148
135
  mediaManagerId: fieldConfigConfig.options?.id ?? "default",
149
- model: entity.model.modelname,
136
+ model: modelResource.model.modelname,
150
137
  widgetName: field,
151
138
  modelId: id
152
139
  });
153
140
  }
154
141
  }
155
- const props = inertiaAddHelper(req, entity, fields, record);
142
+ const props = inertiaAddHelper(req, modelResource, fields, record);
156
143
  if (req.query?.without_layout) {
157
144
  return res.json({
158
145
  props: props
@@ -1 +1 @@
1
- export default function editGroup(req: ReqType, res: ResType): Promise<void | import("express").Response<any, Record<string, any>>>;
1
+ export default function editGroup(req: ReqType, res: ResType): Promise<import("express").Response<any, Record<string, any>>>;
@@ -1,33 +1,25 @@
1
1
  import { ControllerHelper } from "../helpers/controllerHelper.js";
2
2
  import { Adminizer } from "../lib/Adminizer.js";
3
3
  import { inertiaGroupHelper } from "../helpers/inertiaGroupHelper.js";
4
- import { redirectToLogin } from '../helpers/inertiaAutHelper.js';
5
4
  export default async function editGroup(req, res) {
6
- let entity = ControllerHelper.findEntityObject(req);
7
- if (req.adminizer.config.auth.enable) {
8
- if (!req.user) {
9
- return redirectToLogin(req, res);
10
- }
11
- else if (!req.adminizer.accessRightsHelper.hasPermission(`update-${entity.name}-model`, req.user)) {
12
- return res.sendStatus(403);
13
- }
14
- }
5
+ let modelResource = ControllerHelper.findModelResource(req);
6
+ const internalUsers = req.adminizer.modelHandler.internal("users");
7
+ const userModel = internalUsers.get("UserAP");
8
+ const groupModel = internalUsers.get("GroupAP");
15
9
  //Check id
16
10
  if (!req.params.id) {
17
11
  return res.status(404).send({ error: 'Not Found' });
18
12
  }
19
13
  let users;
20
14
  try {
21
- // TODO refactor CRUD functions for DataAccessor usage
22
- users = await req.adminizer.modelHandler.model.get("UserAP")["_find"](({ isAdministrator: false }));
15
+ users = await userModel.find({ where: { isAdministrator: false } });
23
16
  }
24
17
  catch (e) {
25
18
  Adminizer.log.error(e);
26
19
  }
27
20
  let group;
28
21
  try {
29
- // TODO refactor CRUD functions for DataAccessor usage
30
- group = await req.adminizer.modelHandler.model.get("GroupAP")["_findOne"]({ id: req.params.id });
22
+ group = await groupModel.findOne({ where: { id: Number(req.params.id) } });
31
23
  }
32
24
  catch (e) {
33
25
  Adminizer.log.error('Admin edit error: ');
@@ -62,8 +54,7 @@ export default async function editGroup(req, res) {
62
54
  }
63
55
  let updatedGroup;
64
56
  try {
65
- // TODO refactor CRUD functions for DataAccessor usage
66
- updatedGroup = await req.adminizer.modelHandler.model.get("GroupAP")["_updateOne"]({ id: group.id }, {
57
+ updatedGroup = await groupModel.updateOne({ where: { id: group.id } }, {
67
58
  name: req.body.name, description: req.body.description,
68
59
  users: usersInThisGroup, tokens: tokensOfThisGroup
69
60
  });
@@ -79,8 +70,7 @@ export default async function editGroup(req, res) {
79
70
  }
80
71
  if (reloadNeeded) {
81
72
  try {
82
- // TODO refactor CRUD functions for DataAccessor usage
83
- group = await req.adminizer.modelHandler.model.get("GroupAP")["_findOne"]({ id: req.params.id });
73
+ group = await groupModel.findOne({ where: { id: Number(req.params.id) } });
84
74
  }
85
75
  catch (e) {
86
76
  Adminizer.log.error('Admin edit error: ');
@@ -88,14 +78,13 @@ export default async function editGroup(req, res) {
88
78
  return res.status(500).send({ error: 'Internal Server Error' });
89
79
  }
90
80
  try {
91
- // TODO refactor CRUD functions for DataAccessor usage
92
- users = await req.adminizer.modelHandler.model.get("UserAP")["_find"]({ isAdministrator: false });
81
+ users = await userModel.find({ where: { isAdministrator: false } });
93
82
  }
94
83
  catch (e) {
95
84
  Adminizer.log.error(e);
96
85
  }
97
86
  }
98
- const props = inertiaGroupHelper(entity, req, users, groupedTokens, group);
87
+ const props = inertiaGroupHelper(modelResource, req, users, groupedTokens, group);
99
88
  return req.Inertia.render({
100
89
  component: 'add-group',
101
90
  props: props
@@ -1 +1 @@
1
- export default function (req: ReqType, res: ResType): Promise<void | import("express").Response<any, Record<string, any>>>;
1
+ export default function (req: ReqType, res: ResType): Promise<import("express").Response<any, Record<string, any>>>;
@@ -2,25 +2,18 @@ import { ControllerHelper } from "../helpers/controllerHelper.js";
2
2
  import { Adminizer } from "../lib/Adminizer.js";
3
3
  import { generate } from "password-hash";
4
4
  import { inertiaUserHelper } from "../helpers/inertiaUserHelper.js";
5
- import { redirectToLogin } from '../helpers/inertiaAutHelper.js';
6
5
  export default async function (req, res) {
7
- let entity = ControllerHelper.findEntityObject(req);
8
- if (req.adminizer.config.auth.enable) {
9
- if (!req.user) {
10
- return redirectToLogin(req, res);
11
- }
12
- else if (!req.adminizer.accessRightsHelper.hasPermission(`update-${entity.name}-model`, req.user)) {
13
- return res.sendStatus(403);
14
- }
15
- }
6
+ let modelResource = ControllerHelper.findModelResource(req);
7
+ const internalUsers = req.adminizer.modelHandler.internal("users");
8
+ const userModel = internalUsers.get("UserAP");
9
+ const groupModel = internalUsers.get("GroupAP");
16
10
  // Check id
17
11
  if (!req.params.id) {
18
12
  return res.status(404).send({ error: 'Not Found' });
19
13
  }
20
14
  let user;
21
15
  try {
22
- // TODO refactor CRUD functions for DataAccessor usage
23
- user = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ id: req.params.id });
16
+ user = await userModel.findOne({ where: { id: Number(req.params.id) } });
24
17
  }
25
18
  catch (e) {
26
19
  Adminizer.log.error('Admin edit error: ');
@@ -29,8 +22,7 @@ export default async function (req, res) {
29
22
  }
30
23
  let groups;
31
24
  try {
32
- // TODO refactor CRUD functions for DataAccessor usage
33
- groups = await req.adminizer.modelHandler.model.get("GroupAP")["_find"]({});
25
+ groups = await groupModel.find({});
34
26
  }
35
27
  catch (e) {
36
28
  Adminizer.log.error(e);
@@ -55,8 +47,7 @@ export default async function (req, res) {
55
47
  }
56
48
  let updatedUser;
57
49
  try {
58
- // TODO refactor CRUD functions for DataAccessor usage
59
- updatedUser = await req.adminizer.modelHandler.model.get("UserAP")["_updateOne"]({ id: user.id }, {
50
+ updatedUser = await userModel.updateOne({ where: { id: user.id } }, {
60
51
  login: req.body.login, fullName: req.body.fullName,
61
52
  email: req.body.email, timezone: req.body.timezone, expires: req.body.date,
62
53
  locale: locale, isAdministrator: isAdministrator, isConfirmed: isConfirmed, groups: userGroups
@@ -64,8 +55,7 @@ export default async function (req, res) {
64
55
  if (req.body.userPassword) {
65
56
  let passwordHashed = generate(req.body.login + req.body.userPassword + process.env.AP_PASSWORD_SALT);
66
57
  let password = 'masked';
67
- // TODO refactor CRUD functions for DataAccessor usage
68
- updatedUser = await req.adminizer.modelHandler.model.get("UserAP")["_updateOne"]({ id: user.id }, {
58
+ updatedUser = await userModel.updateOne({ where: { id: user.id } }, {
69
59
  login: req.body.login,
70
60
  passwordHashed: passwordHashed
71
61
  });
@@ -82,8 +72,7 @@ export default async function (req, res) {
82
72
  }
83
73
  if (reloadNeeded) {
84
74
  try {
85
- // TODO refactor CRUD functions for DataAccessor usage
86
- user = await req.adminizer.modelHandler.model.get("UserAP")["_findOne"]({ id: req.params.id });
75
+ user = await userModel.findOne({ where: { id: Number(req.params.id) } });
87
76
  }
88
77
  catch (e) {
89
78
  Adminizer.log.error('Admin edit error: ');
@@ -91,14 +80,13 @@ export default async function (req, res) {
91
80
  return res.status(500).send({ error: 'Internal Server Error' });
92
81
  }
93
82
  try {
94
- // TODO refactor CRUD functions for DataAccessor usage
95
- groups = await req.adminizer.modelHandler.model.get("GroupAP")["_find"]({});
83
+ groups = await groupModel.find({});
96
84
  }
97
85
  catch (e) {
98
86
  Adminizer.log.error(e);
99
87
  }
100
88
  }
101
- const props = inertiaUserHelper(entity, req, groups, user);
89
+ const props = inertiaUserHelper(modelResource, req, groups, user);
102
90
  return req.Inertia.render({
103
91
  component: 'add-user',
104
92
  props: props
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Main export controller
3
+ * POST /adminizer/model/:model/export
4
+ * Body: { filterId, format, fields }
5
+ */
6
+ export default function exportData(req: ReqType, res: ResType): Promise<ResType>;
@@ -0,0 +1,315 @@
1
+ import { ControllerHelper } from "../helpers/controllerHelper.js";
2
+ import { DataAccessor } from "../lib/DataAccessor.js";
3
+ import { ListQueryBuilder } from "../lib/list-query-builder/ListQueryBuilder.js";
4
+ import { Adminizer } from "../lib/Adminizer.js";
5
+ import { FilterService } from "../lib/filters/FilterService.js";
6
+ import { convertDatetimeConditions } from "../helpers/filterDatetimeHelper.js";
7
+ /**
8
+ * Main export controller
9
+ * POST /adminizer/model/:model/export
10
+ * Body: { filterId, format, fields }
11
+ */
12
+ export default async function exportData(req, res) {
13
+ const modelResource = ControllerHelper.findModelResource(req);
14
+ if (!modelResource.model) {
15
+ return res.status(404).json({ error: 'Model not found' });
16
+ }
17
+ const { filterId, format, selectedFields: bodySelectedFields } = req.body;
18
+ // Validate format
19
+ const exportFormat = format || 'json';
20
+ if (!['json', 'csv', 'xlsx'].includes(exportFormat)) {
21
+ return res.status(400).json({ error: 'Invalid export format. Supported: json, csv, xlsx' });
22
+ }
23
+ const dataAccessor = new DataAccessor(req.adminizer, req.user, modelResource, "list");
24
+ const fields = dataAccessor.getFieldsConfig();
25
+ // Load filter if provided
26
+ let filters = [];
27
+ let savedColumns = [];
28
+ if (filterId) {
29
+ try {
30
+ if (filterId === 'temporary' && req.session?.temporaryFilters?.[modelResource.name]) {
31
+ const tempFilter = req.session.temporaryFilters[modelResource.name];
32
+ filters = convertDatetimeConditions(tempFilter.conditions || [], { dropEmptyValues: true });
33
+ if (tempFilter.columns && Array.isArray(tempFilter.columns)) {
34
+ savedColumns = tempFilter.columns.map((col, index) => ({
35
+ id: index + 1,
36
+ filter: 'temporary',
37
+ fieldName: col.fieldName,
38
+ order: col.order !== undefined ? col.order : index
39
+ }));
40
+ }
41
+ }
42
+ else if (filterId !== 'temporary') {
43
+ const filterService = new FilterService(req.adminizer);
44
+ const result = await filterService.getFilterWithColumns(filterId, req.user);
45
+ if (result.filter && result.filter.conditions) {
46
+ filters = convertDatetimeConditions(result.filter.conditions, { dropEmptyValues: true });
47
+ }
48
+ savedColumns = result.columns || [];
49
+ }
50
+ }
51
+ catch (err) {
52
+ Adminizer.log.error('Error loading filter for export:', err);
53
+ return res.status(500).json({ error: 'Error loading filter' });
54
+ }
55
+ }
56
+ // Determine fields to export
57
+ let displayFields = fields;
58
+ // Apply selected fields from request body
59
+ if (bodySelectedFields && Array.isArray(bodySelectedFields) && bodySelectedFields.length > 0) {
60
+ const validFields = bodySelectedFields.filter(f => fields[f]);
61
+ if (validFields.length > 0) {
62
+ displayFields = validFields.reduce((acc, key) => {
63
+ if (fields[key]) {
64
+ acc[key] = fields[key];
65
+ }
66
+ return acc;
67
+ }, {});
68
+ }
69
+ }
70
+ // Apply custom columns from filter
71
+ if (savedColumns.length > 0) {
72
+ displayFields = applyCustomColumnsForExport(displayFields, savedColumns);
73
+ }
74
+ // Fetch ALL data (no pagination)
75
+ const queryParams = {
76
+ page: 1,
77
+ limit: 10000, // Large limit to get all records
78
+ filters: filters.length > 0 ? filters : undefined,
79
+ sort: 'id',
80
+ sortDirection: 'ASC',
81
+ fields: Object.keys(displayFields)
82
+ };
83
+ const listQueryBuilder = new ListQueryBuilder(modelResource.model, fields, dataAccessor, req.adminizer.customFilterHandler);
84
+ try {
85
+ const result = await listQueryBuilder.execute(queryParams);
86
+ const records = result.data;
87
+ // Prepare human-readable data for export
88
+ const exportData = prepareExportData(records, displayFields, req);
89
+ // Generate file based on format
90
+ switch (exportFormat) {
91
+ case 'json':
92
+ return sendJsonExport(res, exportData, modelResource.name);
93
+ case 'csv':
94
+ return sendCsvExport(res, exportData, modelResource.name, displayFields, req);
95
+ case 'xlsx':
96
+ return sendXlsxExport(res, exportData, modelResource.name, displayFields, req);
97
+ default:
98
+ return res.status(400).json({ error: 'Invalid export format' });
99
+ }
100
+ }
101
+ catch (err) {
102
+ Adminizer.log.error('Export error:', err);
103
+ return res.status(500).json({ error: 'Export failed', message: err.message });
104
+ }
105
+ }
106
+ /**
107
+ * Apply custom column configuration for export
108
+ */
109
+ function applyCustomColumnsForExport(fields, columns) {
110
+ const sortedColumns = [...columns].sort((a, b) => (a.order || 0) - (b.order || 0));
111
+ const visibleColumns = sortedColumns.filter(col => fields[col.fieldName]);
112
+ if (visibleColumns.length === 0) {
113
+ return fields;
114
+ }
115
+ const result = {};
116
+ for (const col of visibleColumns) {
117
+ if (fields[col.fieldName]) {
118
+ result[col.fieldName] = fields[col.fieldName];
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+ /**
124
+ * Prepare data for export - convert values to human-readable format
125
+ */
126
+ function prepareExportData(records, displayFields, req) {
127
+ return records.map(record => {
128
+ const row = {};
129
+ for (const [key, field] of Object.entries(displayFields)) {
130
+ const fieldConfig = field;
131
+ const config = fieldConfig.config;
132
+ const title = (typeof config === 'object' && config !== null && config.title)
133
+ ? req.i18n.__(config.title)
134
+ : key;
135
+ const value = record[key];
136
+ // Format value based on field type
137
+ row[title] = formatExportValue(value, fieldConfig);
138
+ }
139
+ return row;
140
+ });
141
+ }
142
+ /**
143
+ * Format value for export based on type
144
+ */
145
+ function formatExportValue(value, field) {
146
+ if (value === null || value === undefined) {
147
+ return '';
148
+ }
149
+ const fieldType = field.model?.type;
150
+ // Handle relationships
151
+ if (field.model?.model) {
152
+ const displayField = field.modelConfig?.titleField ??
153
+ (typeof value === 'object' && value ? (value.title || value.name || value.id) : value);
154
+ return typeof displayField === 'object' ? JSON.stringify(displayField) : String(displayField);
155
+ }
156
+ // Handle associations (hasMany)
157
+ if (fieldType === 'association-many' || fieldType === 'association') {
158
+ if (Array.isArray(value)) {
159
+ return value.map((item) => {
160
+ if (typeof item === 'object' && item) {
161
+ return item.title || item.name || item.id || JSON.stringify(item);
162
+ }
163
+ return String(item);
164
+ }).join(', ');
165
+ }
166
+ return String(value);
167
+ }
168
+ // Handle JSON fields
169
+ if (fieldType === 'json') {
170
+ if (typeof value === 'object') {
171
+ return JSON.stringify(value, null, 2);
172
+ }
173
+ return String(value);
174
+ }
175
+ // Handle boolean
176
+ if (fieldType === 'boolean') {
177
+ return value ? 'Yes' : 'No';
178
+ }
179
+ // Handle dates
180
+ if (fieldType === 'date' || fieldType === 'datetime') {
181
+ if (value instanceof Date) {
182
+ return value.toISOString();
183
+ }
184
+ return String(value);
185
+ }
186
+ // Handle arrays
187
+ if (Array.isArray(value)) {
188
+ return value.join(', ');
189
+ }
190
+ // Default: convert to string
191
+ return String(value);
192
+ }
193
+ /**
194
+ * Send JSON export response
195
+ */
196
+ function sendJsonExport(res, data, modelName) {
197
+ const filename = `${modelName}_export_${Date.now()}.json`;
198
+ res.setHeader('Content-Type', 'application/json');
199
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
200
+ res.setHeader('Content-Length', Buffer.byteLength(JSON.stringify(data, null, 2)));
201
+ return res.json(data);
202
+ }
203
+ /**
204
+ * Escape value for CSV
205
+ */
206
+ function escapeCsvValue(value) {
207
+ if (value === null || value === undefined) {
208
+ return '';
209
+ }
210
+ const str = String(value);
211
+ // If value contains comma, newline, or quote, wrap in quotes
212
+ if (str.includes(',') || str.includes('\n') || str.includes('"') || str.includes('\r')) {
213
+ return `"${str.replace(/"/g, '""')}"`;
214
+ }
215
+ return str;
216
+ }
217
+ /**
218
+ * Send CSV export response
219
+ */
220
+ function sendCsvExport(res, data, modelName, fields, req) {
221
+ const filename = `${modelName}_export_${Date.now()}.csv`;
222
+ // Get headers (preserve order from fields)
223
+ const headers = Object.keys(fields).map(key => {
224
+ const field = fields[key];
225
+ const config = field.config;
226
+ return (typeof config === 'object' && config !== null && config.title)
227
+ ? req.i18n.__(config.title)
228
+ : key;
229
+ });
230
+ // Build CSV content
231
+ const csvRows = [];
232
+ // Add BOM for Excel compatibility with UTF-8
233
+ const bom = '\uFEFF';
234
+ // Header row
235
+ csvRows.push(headers.map(h => escapeCsvValue(h)).join(','));
236
+ // Data rows
237
+ for (const row of data) {
238
+ const values = headers.map(header => {
239
+ const value = row[header];
240
+ return escapeCsvValue(value);
241
+ });
242
+ csvRows.push(values.join(','));
243
+ }
244
+ const csvContent = bom + csvRows.join('\r\n');
245
+ const buffer = Buffer.from(csvContent, 'utf-8');
246
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
247
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
248
+ res.setHeader('Content-Length', buffer.length);
249
+ return res.send(buffer);
250
+ }
251
+ /**
252
+ * Send XLSX export response
253
+ * Uses xlsx library if available
254
+ */
255
+ async function sendXlsxExport(res, data, modelName, fields, req) {
256
+ let XLSX;
257
+ try {
258
+ XLSX = await import('xlsx');
259
+ }
260
+ catch (err) {
261
+ Adminizer.log.error('xlsx library not installed. Install with: npm install xlsx');
262
+ return res.status(500).json({
263
+ error: 'XLSX export requires xlsx library',
264
+ message: 'Please install xlsx: npm install xlsx'
265
+ });
266
+ }
267
+ const filename = `${modelName}_export_${Date.now()}.xlsx`;
268
+ // Get headers (preserve order from fields)
269
+ const headers = Object.keys(fields).map(key => {
270
+ const field = fields[key];
271
+ const config = field.config;
272
+ return (typeof config === 'object' && config !== null && config.title)
273
+ ? req.i18n.__(config.title)
274
+ : key;
275
+ });
276
+ // Create workbook
277
+ const wb = XLSX.default.utils.book_new();
278
+ // Prepare data array with headers
279
+ const wsData = [headers];
280
+ // Add rows
281
+ for (const row of data) {
282
+ const values = headers.map(header => {
283
+ const value = row[header];
284
+ if (value === '' || value === null || value === undefined) {
285
+ return null;
286
+ }
287
+ return value;
288
+ });
289
+ wsData.push(values);
290
+ }
291
+ // Create worksheet
292
+ const ws = XLSX.default.utils.aoa_to_sheet(wsData);
293
+ // Auto-size columns (optional)
294
+ const colWidths = headers.map((header, i) => {
295
+ let maxWidth = header.length;
296
+ for (const row of data) {
297
+ const val = String(row[header] || '');
298
+ if (val.length > maxWidth) {
299
+ maxWidth = val.length;
300
+ }
301
+ }
302
+ // Cap at 50 characters, minimum 10
303
+ return { wch: Math.min(Math.max(maxWidth + 2, 10), 50) };
304
+ });
305
+ ws['!cols'] = colWidths;
306
+ // Add worksheet to workbook
307
+ const sheetName = modelName.substring(0, 31); // Excel sheet name limit
308
+ XLSX.default.utils.book_append_sheet(wb, ws, sheetName);
309
+ // Generate buffer
310
+ const buffer = XLSX.default.write(wb, { type: 'buffer', bookType: 'xlsx' });
311
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
312
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
313
+ res.setHeader('Content-Length', buffer.length);
314
+ return res.send(buffer);
315
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Public feed controller — exports data by apiKey with userKey authentication
3
+ * GET /adminizer/api/feed/:apiKey.json?userKey=xxx
4
+ * GET /adminizer/api/feed/:apiKey.xml?userKey=xxx
5
+ */
6
+ export default function feedController(req: ReqType, res: ResType): Promise<ResType>;