@goplusvn/core 0.1.0 → 0.1.1

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 (369) hide show
  1. package/package.json +2 -1
  2. package/src/assets/erp_wallpaper.png +0 -0
  3. package/src/assets/goeat_logo.png +0 -0
  4. package/src/audit/audit-manager.ts +139 -0
  5. package/src/audit/index.ts +11 -0
  6. package/src/audit/memory-audit-logger.ts +86 -0
  7. package/src/audit/types.ts +50 -0
  8. package/src/auth/auth-service.ts +97 -0
  9. package/src/auth/index.ts +266 -0
  10. package/src/code-generation/index.ts +69 -0
  11. package/src/configs/auth-routes.ts +17 -0
  12. package/src/configs/crud.ts +136 -0
  13. package/src/configs/data/navigations.ts +781 -0
  14. package/src/configs/data/oauth-links.ts +10 -0
  15. package/src/configs/entities/material-categories.config.ts +125 -0
  16. package/src/configs/i18n.ts +12 -0
  17. package/src/configs/index.ts +26 -0
  18. package/src/configs/status.ts +25 -0
  19. package/src/configs/themes.ts +100 -0
  20. package/src/crud/components/crud-bulk-actions.tsx +91 -0
  21. package/src/crud/components/crud-card-view.tsx +241 -0
  22. package/src/crud/components/crud-context.tsx +122 -0
  23. package/src/crud/components/crud-delete-dialog.tsx +145 -0
  24. package/src/crud/components/crud-dialog.tsx +406 -0
  25. package/src/crud/components/crud-empty-state.tsx +104 -0
  26. package/src/crud/components/crud-export-button.tsx +170 -0
  27. package/src/crud/components/crud-field-renderer.tsx +653 -0
  28. package/src/crud/components/crud-filter-chips.tsx +102 -0
  29. package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
  30. package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
  31. package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
  32. package/src/crud/components/crud-filters/index.tsx +76 -0
  33. package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
  34. package/src/crud/components/crud-filters/select-filter.tsx +141 -0
  35. package/src/crud/components/crud-filters/text-filter.tsx +86 -0
  36. package/src/crud/components/crud-form.tsx +642 -0
  37. package/src/crud/components/crud-import-dialog.tsx +440 -0
  38. package/src/crud/components/crud-infinite-scroll.tsx +116 -0
  39. package/src/crud/components/crud-page.tsx +1017 -0
  40. package/src/crud/components/crud-provider.tsx +277 -0
  41. package/src/crud/components/crud-row-actions.tsx +189 -0
  42. package/src/crud/components/crud-search.tsx +82 -0
  43. package/src/crud/components/crud-sheet.tsx +336 -0
  44. package/src/crud/components/crud-table-skeleton.tsx +26 -0
  45. package/src/crud/components/crud-table-toolbar.tsx +91 -0
  46. package/src/crud/components/crud-table.tsx +352 -0
  47. package/src/crud/components/crud-virtual-table.tsx +55 -0
  48. package/src/crud/components/index.tsx +20 -0
  49. package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
  50. package/src/crud/crud-filters/datetime-filter.tsx +82 -0
  51. package/src/crud/crud-filters/filter-builder.tsx +64 -0
  52. package/src/crud/crud-filters/index.tsx +78 -0
  53. package/src/crud/crud-filters/radio-filter.tsx +79 -0
  54. package/src/crud/crud-filters/select-filter.tsx +148 -0
  55. package/src/crud/crud-filters/text-filter.tsx +81 -0
  56. package/src/crud/index.ts +43 -0
  57. package/src/crud/lib/crud-service.test.ts +334 -0
  58. package/src/crud/lib/crud-service.ts +358 -0
  59. package/src/crud/lib/crud-utils.test.ts +354 -0
  60. package/src/crud/lib/crud-utils.ts +299 -0
  61. package/src/crud/lib/crud-validator.ts +247 -0
  62. package/src/crud/lib/data-loader.ts +234 -0
  63. package/src/crud/lib/field-calculator.ts +241 -0
  64. package/src/crud/lib/field-formatter.ts +240 -0
  65. package/src/crud/lib/import-export-service.test.ts +290 -0
  66. package/src/crud/lib/import-export-service.ts +352 -0
  67. package/src/crud/lib/import-server-utils.ts +109 -0
  68. package/src/crud/lib/lazy-loader.ts +241 -0
  69. package/src/crud/lib/parse-filters.ts +85 -0
  70. package/src/crud/lib/permissions.ts +52 -0
  71. package/src/crud/lib/serialize-config.ts +60 -0
  72. package/src/crud/lib/stream-loader.ts +145 -0
  73. package/src/crud/lib/translate-config.ts +335 -0
  74. package/src/crud/lib/types.ts +11 -0
  75. package/src/crud/pages/entity-crud-page.tsx +144 -0
  76. package/src/crud/server.ts +8 -0
  77. package/src/home/constants.tsx +142 -0
  78. package/src/home/feature-showcase.tsx +171 -0
  79. package/src/home/home-page.tsx +191 -0
  80. package/src/home/hooks/index.ts +1 -0
  81. package/src/home/hooks/useWidgetPreferences.ts +167 -0
  82. package/src/home/index.ts +33 -0
  83. package/src/home/quick-access-dialog.tsx +271 -0
  84. package/src/home/quick-access-menu.tsx +267 -0
  85. package/src/home/types.ts +140 -0
  86. package/src/home/welcome-card.tsx +92 -0
  87. package/src/home/widget-container.tsx +258 -0
  88. package/src/home/widgets/base-widget.tsx +200 -0
  89. package/src/home/widgets/customers-widget.tsx +74 -0
  90. package/src/home/widgets/index.ts +6 -0
  91. package/src/home/widgets/orders-widget.tsx +87 -0
  92. package/src/home/widgets/revenue-widget.tsx +71 -0
  93. package/src/home/widgets/stock-widget.tsx +109 -0
  94. package/src/hooks/index.tsx +598 -0
  95. package/src/hooks/use-tenant.test.tsx +30 -0
  96. package/src/hooks/use-tenant.ts +5 -0
  97. package/src/index.ts +17 -0
  98. package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
  99. package/src/infrastructure/api-service.ts +317 -0
  100. package/src/infrastructure/cache/cache-manager.ts +107 -0
  101. package/src/infrastructure/cache/cache.ts +120 -0
  102. package/src/infrastructure/cache/index.ts +8 -0
  103. package/src/infrastructure/cache/types.ts +48 -0
  104. package/src/infrastructure/cron/cron-manager.ts +239 -0
  105. package/src/infrastructure/cron/index.ts +6 -0
  106. package/src/infrastructure/cron/types.ts +41 -0
  107. package/src/infrastructure/event-bus/event-bus.ts +145 -0
  108. package/src/infrastructure/event-bus/index.ts +2 -0
  109. package/src/infrastructure/event-bus/types.ts +22 -0
  110. package/src/infrastructure/index.ts +32 -0
  111. package/src/infrastructure/lock/decorators.ts +67 -0
  112. package/src/infrastructure/lock/index.ts +2 -0
  113. package/src/infrastructure/lock/lock-manager.ts +33 -0
  114. package/src/infrastructure/logger/index.ts +2 -0
  115. package/src/infrastructure/logger/logger.ts +96 -0
  116. package/src/infrastructure/logger/types.ts +25 -0
  117. package/src/layout/index.tsx +185 -0
  118. package/src/navigation/index.ts +91 -0
  119. package/src/notification/index.ts +14 -0
  120. package/src/notification/notification-service.ts +120 -0
  121. package/src/notification/storage/in-memory.ts +56 -0
  122. package/src/notification/storage/index.ts +1 -0
  123. package/src/notification/types.ts +51 -0
  124. package/src/organization/branch-service.ts +299 -0
  125. package/src/organization/branches.config.ts +154 -0
  126. package/src/organization/index.ts +5 -0
  127. package/src/plugin/apps-registry.ts +97 -0
  128. package/src/plugin/index.ts +5 -0
  129. package/src/plugin/types.ts +41 -0
  130. package/src/providers/index.tsx +109 -0
  131. package/src/providers/tenant-provider.tsx +45 -0
  132. package/src/rbac/components/roles/role-card.tsx +158 -0
  133. package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
  134. package/src/rbac/components/roles/role-toolbar.tsx +123 -0
  135. package/src/rbac/hooks/use-role-operations.ts +159 -0
  136. package/src/rbac/hooks/use-roles-data.ts +59 -0
  137. package/src/rbac/index.ts +297 -0
  138. package/src/rbac/lib/permission-helpers.ts +63 -0
  139. package/src/rbac/pages/action-list-page.tsx +25 -0
  140. package/src/rbac/pages/resource-list-page.tsx +25 -0
  141. package/src/rbac/pages/role-list-page.tsx +378 -0
  142. package/src/rbac/permission-service.ts +140 -0
  143. package/src/rbac/permissions.ts +135 -0
  144. package/src/rbac/resource-service.ts +115 -0
  145. package/src/rbac/resource-validator.ts +119 -0
  146. package/src/rbac/role-service.ts +165 -0
  147. package/src/rbac/server.ts +16 -0
  148. package/src/rbac/types.ts +38 -0
  149. package/src/schemas/action.schema.ts +66 -0
  150. package/src/schemas/branch.schema.ts +52 -0
  151. package/src/schemas/coming-soon-schema.ts +9 -0
  152. package/src/schemas/company.schema.ts +44 -0
  153. package/src/schemas/forgot-passward-schema.ts +9 -0
  154. package/src/schemas/index.ts +30 -0
  155. package/src/schemas/material-category.schema.ts +43 -0
  156. package/src/schemas/material-pricing.schema.ts +74 -0
  157. package/src/schemas/material.schema.ts +76 -0
  158. package/src/schemas/materials.ts +52 -0
  159. package/src/schemas/new-passward-schema.ts +15 -0
  160. package/src/schemas/partner-company.schema.ts +149 -0
  161. package/src/schemas/register-schema.ts +36 -0
  162. package/src/schemas/resource.schema.ts +133 -0
  163. package/src/schemas/role.schema.ts +11 -0
  164. package/src/schemas/sign-in-schema.ts +24 -0
  165. package/src/schemas/supplier-pricing.schema.ts +15 -0
  166. package/src/schemas/supplier.schema.ts +120 -0
  167. package/src/schemas/system-category-group.schema.ts +67 -0
  168. package/src/schemas/system-category.schema.ts +77 -0
  169. package/src/schemas/system-config.schema.ts +118 -0
  170. package/src/schemas/uom.schema.ts +75 -0
  171. package/src/schemas/user-supplier.schema.ts +179 -0
  172. package/src/schemas/user.schema.ts +18 -0
  173. package/src/schemas/verify-email-schema.ts +9 -0
  174. package/src/schemas/warehouse.schema.ts +49 -0
  175. package/src/system/components/categories/category-list.tsx +529 -0
  176. package/src/system/components/categories/category-manager.tsx +89 -0
  177. package/src/system/components/categories/group-sidebar.tsx +308 -0
  178. package/src/system/components/settings/setting-dialogs.tsx +197 -0
  179. package/src/system/components/settings/setting-field.tsx +291 -0
  180. package/src/system/components/settings/setting-form-dialog.tsx +308 -0
  181. package/src/system/components/settings/settings-groups.ts +80 -0
  182. package/src/system/components/settings/settings-search.tsx +71 -0
  183. package/src/system/components/settings/settings-section.tsx +74 -0
  184. package/src/system/components/settings/settings-sidebar.tsx +81 -0
  185. package/src/system/constants.ts +3 -0
  186. package/src/system/index.ts +150 -0
  187. package/src/system/job-manager.ts +176 -0
  188. package/src/system/pages/components/categories/category-list.tsx +537 -0
  189. package/src/system/pages/components/categories/category-manager.tsx +90 -0
  190. package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
  191. package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
  192. package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
  193. package/src/system/pages/components/settings/setting-field.tsx +292 -0
  194. package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
  195. package/src/system/pages/components/settings/settings-groups.ts +87 -0
  196. package/src/system/pages/components/settings/settings-page.tsx +372 -0
  197. package/src/system/pages/components/settings/settings-search.tsx +71 -0
  198. package/src/system/pages/components/settings/settings-section.tsx +74 -0
  199. package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
  200. package/src/system/pages/components/settings/system-settings.tsx +244 -0
  201. package/src/system/pages/system-category-page.tsx +15 -0
  202. package/src/system/pages/system-settings-page.tsx +380 -0
  203. package/src/system/schemas/system-category-group.schema.ts +46 -0
  204. package/src/system/schemas/system-category.schema.ts +56 -0
  205. package/src/system/services/settings-service.ts +127 -0
  206. package/src/system/services/system-category-service.ts +63 -0
  207. package/src/system/types.ts +45 -0
  208. package/src/types/index.ts +703 -0
  209. package/src/ui/auth/auth-layout.tsx +135 -0
  210. package/src/ui/auth/forgot-password-form.tsx +98 -0
  211. package/src/ui/auth/index.tsx +7 -0
  212. package/src/ui/auth/new-password-form.tsx +107 -0
  213. package/src/ui/auth/oauth-links.tsx +30 -0
  214. package/src/ui/auth/register-form.tsx +202 -0
  215. package/src/ui/auth/sign-in-form.tsx +238 -0
  216. package/src/ui/auth/verify-email-form.tsx +104 -0
  217. package/src/ui/crud/index.tsx +10 -0
  218. package/src/ui/data-display/accordion.tsx +65 -0
  219. package/src/ui/data-display/aspect-ratio.tsx +11 -0
  220. package/src/ui/data-display/avatar.tsx +163 -0
  221. package/src/ui/data-display/bento-grid.tsx +77 -0
  222. package/src/ui/data-display/carousel.tsx +249 -0
  223. package/src/ui/data-display/chart.tsx +363 -0
  224. package/src/ui/data-display/code-block-highlight.tsx +54 -0
  225. package/src/ui/data-display/collapsible.tsx +42 -0
  226. package/src/ui/data-display/compact-stat-bar.tsx +149 -0
  227. package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
  228. package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
  229. package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
  230. package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
  231. package/src/ui/data-display/data-table/data-table.tsx +597 -0
  232. package/src/ui/data-display/data-table/index.ts +44 -0
  233. package/src/ui/data-display/data-table-column-header.tsx +75 -0
  234. package/src/ui/data-display/data-table-pagination.tsx +130 -0
  235. package/src/ui/data-display/data-table-view-options.tsx +59 -0
  236. package/src/ui/data-display/formatted-number-input.tsx +210 -0
  237. package/src/ui/data-display/highlight.tsx +20 -0
  238. package/src/ui/data-display/hover-card.tsx +48 -0
  239. package/src/ui/data-display/index.tsx +50 -0
  240. package/src/ui/data-display/iphone-15-pro.tsx +114 -0
  241. package/src/ui/data-display/kanban/index.ts +4 -0
  242. package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
  243. package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
  244. package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
  245. package/src/ui/data-display/kanban/kanban-types.ts +21 -0
  246. package/src/ui/data-display/kpi-card.tsx +68 -0
  247. package/src/ui/data-display/media-grid.tsx +110 -0
  248. package/src/ui/data-display/safari.tsx +175 -0
  249. package/src/ui/data-display/show-more-text.tsx +55 -0
  250. package/src/ui/data-display/tabs.tsx +68 -0
  251. package/src/ui/data-display/timeline.tsx +256 -0
  252. package/src/ui/feedback/alert.tsx +60 -0
  253. package/src/ui/feedback/context-menu.tsx +245 -0
  254. package/src/ui/feedback/drawer.tsx +132 -0
  255. package/src/ui/feedback/error-dialog.tsx +273 -0
  256. package/src/ui/feedback/index.tsx +183 -0
  257. package/src/ui/feedback/progress.tsx +32 -0
  258. package/src/ui/feedback/sheet.tsx +148 -0
  259. package/src/ui/feedback/sonner.tsx +36 -0
  260. package/src/ui/forms/command.tsx +157 -0
  261. package/src/ui/forms/date-picker.tsx +73 -0
  262. package/src/ui/forms/date-range-picker.tsx +76 -0
  263. package/src/ui/forms/date-time-picker.tsx +109 -0
  264. package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
  265. package/src/ui/forms/editor/index.tsx +130 -0
  266. package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
  267. package/src/ui/forms/emoji-picker.tsx +109 -0
  268. package/src/ui/forms/file-dropzone.tsx +169 -0
  269. package/src/ui/forms/file-thumbnail.tsx +29 -0
  270. package/src/ui/forms/index.tsx +201 -0
  271. package/src/ui/forms/input-file.tsx +99 -0
  272. package/src/ui/forms/input-group.tsx +46 -0
  273. package/src/ui/forms/input-otp.tsx +81 -0
  274. package/src/ui/forms/input-phone.tsx +172 -0
  275. package/src/ui/forms/input-spin.tsx +116 -0
  276. package/src/ui/forms/input-tags.tsx +219 -0
  277. package/src/ui/forms/input-time.tsx +42 -0
  278. package/src/ui/forms/multi-select.tsx +629 -0
  279. package/src/ui/forms/multiple-date-picker.tsx +74 -0
  280. package/src/ui/forms/radio-group.tsx +42 -0
  281. package/src/ui/forms/rating.tsx +158 -0
  282. package/src/ui/forms/time-picker.tsx +57 -0
  283. package/src/ui/index.tsx +17 -0
  284. package/src/ui/layout/animated-list.tsx +77 -0
  285. package/src/ui/layout/animated-sidebar.tsx +294 -0
  286. package/src/ui/layout/command-menu.tsx +355 -0
  287. package/src/ui/layout/customizer.tsx +324 -0
  288. package/src/ui/layout/footer.tsx +43 -0
  289. package/src/ui/layout/full-screen-toggle.tsx +52 -0
  290. package/src/ui/layout/header-breadcrumb.tsx +77 -0
  291. package/src/ui/layout/horizontal-layout-header.tsx +83 -0
  292. package/src/ui/layout/horizontal-layout.tsx +50 -0
  293. package/src/ui/layout/index.tsx +25 -0
  294. package/src/ui/layout/language-dropdown.tsx +103 -0
  295. package/src/ui/layout/logo.tsx +63 -0
  296. package/src/ui/layout/main-layout.tsx +57 -0
  297. package/src/ui/layout/mode-dropdown.tsx +58 -0
  298. package/src/ui/layout/notification-dropdown.tsx +127 -0
  299. package/src/ui/layout/page-tabs.tsx +306 -0
  300. package/src/ui/layout/route-cache.tsx +214 -0
  301. package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
  302. package/src/ui/layout/sidebar.tsx +279 -0
  303. package/src/ui/layout/tab-content-cache.tsx +201 -0
  304. package/src/ui/layout/tab-navigation-provider.tsx +536 -0
  305. package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
  306. package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
  307. package/src/ui/layout/user-dropdown.tsx +188 -0
  308. package/src/ui/layout/vertical-layout-header.tsx +65 -0
  309. package/src/ui/layout/vertical-layout.tsx +47 -0
  310. package/src/ui/management/audit-log-page.tsx +209 -0
  311. package/src/ui/management/cache-management.tsx +349 -0
  312. package/src/ui/management/index.ts +3 -0
  313. package/src/ui/management/job-management.tsx +308 -0
  314. package/src/ui/pages/not-found.tsx +30 -0
  315. package/src/ui/primitives/badge.tsx +66 -0
  316. package/src/ui/primitives/breadcrumb.tsx +103 -0
  317. package/src/ui/primitives/button.tsx +129 -0
  318. package/src/ui/primitives/calendar.tsx +74 -0
  319. package/src/ui/primitives/card.tsx +86 -0
  320. package/src/ui/primitives/checkbox.tsx +31 -0
  321. package/src/ui/primitives/client.ts +30 -0
  322. package/src/ui/primitives/combobox.tsx +290 -0
  323. package/src/ui/primitives/dialog.tsx +121 -0
  324. package/src/ui/primitives/dropdown-menu.tsx +239 -0
  325. package/src/ui/primitives/dynamic-icon.tsx +24 -0
  326. package/src/ui/primitives/index.tsx +134 -0
  327. package/src/ui/primitives/input-number.tsx +131 -0
  328. package/src/ui/primitives/input.tsx +22 -0
  329. package/src/ui/primitives/keyboard.tsx +23 -0
  330. package/src/ui/primitives/label.tsx +24 -0
  331. package/src/ui/primitives/menubar.tsx +262 -0
  332. package/src/ui/primitives/navigation-menu.tsx +157 -0
  333. package/src/ui/primitives/pagination.tsx +118 -0
  334. package/src/ui/primitives/popover.tsx +56 -0
  335. package/src/ui/primitives/prefetch-link.tsx +60 -0
  336. package/src/ui/primitives/resizable.tsx +59 -0
  337. package/src/ui/primitives/scroll-area.tsx +63 -0
  338. package/src/ui/primitives/select.tsx +172 -0
  339. package/src/ui/primitives/separator.tsx +51 -0
  340. package/src/ui/primitives/sidebar.tsx +844 -0
  341. package/src/ui/primitives/slider.tsx +27 -0
  342. package/src/ui/primitives/status-badge.tsx +47 -0
  343. package/src/ui/primitives/sticky-layout.tsx +50 -0
  344. package/src/ui/primitives/switch.tsx +29 -0
  345. package/src/ui/primitives/table.tsx +116 -0
  346. package/src/ui/primitives/tabs.tsx +55 -0
  347. package/src/ui/primitives/toggle-group.tsx +70 -0
  348. package/src/ui/primitives/toggle.tsx +47 -0
  349. package/src/ui/primitives/tooltip.tsx +59 -0
  350. package/src/user/components/dangerous-zone.tsx +34 -0
  351. package/src/user/components/delete-account-form.tsx +40 -0
  352. package/src/user/components/index.ts +4 -0
  353. package/src/user/components/profile-info-form.tsx +390 -0
  354. package/src/user/components/profile-info.tsx +32 -0
  355. package/src/user/components/unified-profile-dialog.tsx +1019 -0
  356. package/src/user/components/user-stats.tsx +27 -0
  357. package/src/user/components/user-toolbar.tsx +137 -0
  358. package/src/user/components/users-card-view.tsx +253 -0
  359. package/src/user/index.ts +11 -0
  360. package/src/user/pages/user-list-page.tsx +234 -0
  361. package/src/user/pages/users-client-page.tsx +385 -0
  362. package/src/user/profile-page.tsx +19 -0
  363. package/src/user/schemas.ts +68 -0
  364. package/src/user/types.ts +34 -0
  365. package/src/user/user-service.ts +538 -0
  366. package/src/utils/index.ts +906 -0
  367. package/src/workflow/activity-timeline.tsx +412 -0
  368. package/src/workflow/approval-workflow.tsx +31 -0
  369. package/src/workflow/index.ts +2 -0
@@ -0,0 +1,334 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { crudService } from "./crud-service";
3
+
4
+ // Mock crudConfig
5
+ vi.mock("@/configs/crud", () => ({
6
+ crudConfig: {
7
+ service: {
8
+ cacheEnabled: false,
9
+ cacheBusterEnabled: false,
10
+ },
11
+ },
12
+ }));
13
+
14
+ // Mock logger
15
+ vi.mock("@/lib/logger", () => ({
16
+ logger: {
17
+ debug: vi.fn(),
18
+ error: vi.fn(),
19
+ },
20
+ }));
21
+
22
+ // Save original fetch
23
+ const originalFetch = global.fetch;
24
+
25
+ describe("CrudService", () => {
26
+ const mockEndpoint = "/api/test";
27
+
28
+ beforeEach(() => {
29
+ global.fetch = vi.fn();
30
+ });
31
+
32
+ afterEach(() => {
33
+ global.fetch = originalFetch;
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ describe("fetch", () => {
38
+ it("should fetch data with correct query params", async () => {
39
+ const mockData = { data: [], total: 0, page: 1, pageSize: 10 };
40
+ const mockResponse = {
41
+ ok: true,
42
+ headers: { get: () => "application/json" },
43
+ json: () => Promise.resolve(mockData),
44
+ };
45
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
46
+
47
+ const params = { page: 1, pageSize: 10, search: "test" };
48
+ const result = await crudService.fetch(mockEndpoint, params);
49
+
50
+ expect(global.fetch).toHaveBeenCalledWith(
51
+ expect.stringContaining("/api/test?page=1&pageSize=10&search=test"),
52
+ expect.any(Object),
53
+ );
54
+ expect(result).toEqual(mockData);
55
+ });
56
+
57
+ it("should handle API errors", async () => {
58
+ const mockResponse = {
59
+ ok: false,
60
+ status: 500,
61
+ statusText: "Internal Server Error",
62
+ text: () => Promise.resolve("Error details"),
63
+ headers: { get: () => "text/plain" },
64
+ };
65
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
66
+
67
+ await expect(
68
+ crudService.fetch(mockEndpoint, { page: 1, pageSize: 10 }),
69
+ ).rejects.toThrow("Failed to fetch: Internal Server Error");
70
+ });
71
+
72
+ it("should throw error for invalid content type", async () => {
73
+ const mockResponse = {
74
+ ok: true,
75
+ headers: { get: () => "text/html" },
76
+ text: () => Promise.resolve("<html>Error</html>"),
77
+ };
78
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
79
+
80
+ await expect(
81
+ crudService.fetch(mockEndpoint, { page: 1, pageSize: 10 }),
82
+ ).rejects.toThrow("Invalid response format");
83
+ });
84
+
85
+ it("should handle cache buster when enabled", async () => {
86
+ // Re-mock config for this test
87
+ vi.resetModules();
88
+ vi.doMock("@/configs/crud", () => ({
89
+ crudConfig: {
90
+ service: {
91
+ cacheEnabled: false,
92
+ cacheBusterEnabled: true,
93
+ },
94
+ },
95
+ }));
96
+
97
+ const { crudService: freshService } = await import("./crud-service");
98
+
99
+ const mockResponse = {
100
+ ok: true,
101
+ headers: { get: () => "application/json" },
102
+ json: () => Promise.resolve({ data: [] }),
103
+ };
104
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
105
+
106
+ await freshService.fetch(mockEndpoint, { page: 1, pageSize: 10 });
107
+
108
+ expect(global.fetch).toHaveBeenCalledWith(
109
+ expect.stringMatching(/_t=\d+/),
110
+ expect.any(Object),
111
+ );
112
+ });
113
+ });
114
+
115
+ describe("create", () => {
116
+ it("should create a record successfully", async () => {
117
+ const mockData = { id: 1, name: "Test" };
118
+ const mockResponse = {
119
+ ok: true,
120
+ headers: { get: () => "application/json" },
121
+ json: () => Promise.resolve(mockData),
122
+ };
123
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
124
+
125
+ const payload = { name: "Test" };
126
+ const result = await crudService.create(mockEndpoint, payload);
127
+
128
+ expect(global.fetch).toHaveBeenCalledWith(
129
+ mockEndpoint,
130
+ expect.objectContaining({
131
+ method: "POST",
132
+ body: JSON.stringify(payload),
133
+ }),
134
+ );
135
+ expect(result).toEqual(mockData);
136
+ });
137
+
138
+ it("should handle creation errors with JSON response", async () => {
139
+ const errorResponse = {
140
+ ok: false,
141
+ status: 400,
142
+ statusText: "Bad Request",
143
+ headers: { get: () => "application/json" },
144
+ json: () =>
145
+ Promise.resolve({ message: "Validation failed", field: "name" }),
146
+ };
147
+ vi.mocked(global.fetch).mockResolvedValue(errorResponse as any);
148
+
149
+ try {
150
+ await crudService.create(mockEndpoint, { name: "" });
151
+ } catch (e: any) {
152
+ expect(e.message).toBe("Validation failed");
153
+ expect(e.field).toBe("name");
154
+ }
155
+ });
156
+
157
+ it("should handle creation errors with non-JSON response", async () => {
158
+ const errorResponse = {
159
+ ok: false,
160
+ status: 500,
161
+ statusText: "Error",
162
+ headers: { get: () => "text/plain" },
163
+ text: () => Promise.resolve("Server Error"),
164
+ };
165
+ vi.mocked(global.fetch).mockResolvedValue(errorResponse as any);
166
+
167
+ await expect(crudService.create(mockEndpoint, {})).rejects.toThrow(
168
+ "Server Error",
169
+ );
170
+ });
171
+
172
+ it("should throw for invalid response content type", async () => {
173
+ const mockResponse = {
174
+ ok: true,
175
+ headers: { get: () => "text/html" },
176
+ text: () => Promise.resolve("<html></html>"),
177
+ };
178
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
179
+
180
+ await expect(crudService.create(mockEndpoint, {})).rejects.toThrow(
181
+ "Invalid response format",
182
+ );
183
+ });
184
+ });
185
+
186
+ describe("update", () => {
187
+ it("should update a record successfully", async () => {
188
+ const mockData = { id: "123", name: "Updated" };
189
+ const mockResponse = {
190
+ ok: true,
191
+ headers: { get: () => "application/json" },
192
+ json: () => Promise.resolve(mockData),
193
+ };
194
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
195
+
196
+ const payload = { name: "Updated" };
197
+ const result = await crudService.update(mockEndpoint, "123", payload);
198
+
199
+ expect(global.fetch).toHaveBeenCalledWith(
200
+ `${mockEndpoint}/123`,
201
+ expect.objectContaining({
202
+ method: "PUT",
203
+ body: JSON.stringify(payload),
204
+ }),
205
+ );
206
+ expect(result).toEqual(mockData);
207
+ });
208
+
209
+ it("should handle update errors", async () => {
210
+ const errorResponse = {
211
+ ok: false,
212
+ status: 400,
213
+ headers: { get: () => "application/json" },
214
+ json: () => Promise.resolve({ error: "Update failed" }),
215
+ };
216
+ vi.mocked(global.fetch).mockResolvedValue(errorResponse as any);
217
+
218
+ await expect(crudService.update(mockEndpoint, "123", {})).rejects.toThrow(
219
+ "Update failed",
220
+ );
221
+ });
222
+
223
+ it("should handle non-JSON error response", async () => {
224
+ const errorResponse = {
225
+ ok: false,
226
+ status: 500,
227
+ headers: { get: () => "text/html" },
228
+ text: () => Promise.resolve("<html>Error</html>"),
229
+ url: "/api/test",
230
+ };
231
+ vi.mocked(global.fetch).mockResolvedValue(errorResponse as any);
232
+
233
+ await expect(crudService.update(mockEndpoint, "123", {})).rejects.toThrow(
234
+ "API endpoint returned HTML",
235
+ );
236
+ });
237
+
238
+ it("should throw if success response is not JSON", async () => {
239
+ const mockResponse = {
240
+ ok: true,
241
+ headers: { get: () => "text/plain" },
242
+ text: () => Promise.resolve("ok"),
243
+ };
244
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
245
+
246
+ await expect(crudService.update(mockEndpoint, "123", {})).rejects.toThrow(
247
+ "Invalid response format",
248
+ );
249
+ });
250
+ });
251
+
252
+ describe("delete", () => {
253
+ it("should delete a record successfully", async () => {
254
+ const mockResponse = {
255
+ ok: true,
256
+ };
257
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
258
+
259
+ await crudService.delete(mockEndpoint, "123");
260
+
261
+ expect(global.fetch).toHaveBeenCalledWith(
262
+ `${mockEndpoint}/123`,
263
+ expect.objectContaining({
264
+ method: "DELETE",
265
+ }),
266
+ );
267
+ });
268
+
269
+ it("should handle delete errors", async () => {
270
+ const errorResponse = {
271
+ ok: false,
272
+ status: 404,
273
+ statusText: "Not Found",
274
+ headers: { get: () => "application/json" },
275
+ text: () => Promise.resolve("Not Found"),
276
+ json: () => Promise.resolve({ message: "Not Found" }),
277
+ };
278
+ vi.mocked(global.fetch).mockResolvedValue(errorResponse as any);
279
+
280
+ await expect(crudService.delete(mockEndpoint, "999")).rejects.toThrow(
281
+ "Not Found",
282
+ );
283
+ });
284
+
285
+ it("should handle non-JSON delete errors", async () => {
286
+ const errorResponse = {
287
+ ok: false,
288
+ headers: { get: () => "text/plain" },
289
+ text: () => Promise.resolve("Error"),
290
+ statusText: "Error",
291
+ };
292
+ vi.mocked(global.fetch).mockResolvedValue(errorResponse as any);
293
+
294
+ await expect(crudService.delete(mockEndpoint, "999")).rejects.toThrow(
295
+ "Error",
296
+ );
297
+ });
298
+ });
299
+
300
+ describe("deleteMany", () => {
301
+ it("should delete multiple records successfully", async () => {
302
+ const mockResponse = {
303
+ ok: true,
304
+ };
305
+ vi.mocked(global.fetch).mockResolvedValue(mockResponse as any);
306
+
307
+ const ids = ["1", "2", "3"];
308
+ await crudService.deleteMany(mockEndpoint, ids);
309
+
310
+ expect(global.fetch).toHaveBeenCalledWith(
311
+ `${mockEndpoint}/bulk`,
312
+ expect.objectContaining({
313
+ method: "DELETE",
314
+ body: JSON.stringify({ ids }),
315
+ }),
316
+ );
317
+ });
318
+
319
+ it("should handle bulk delete errors", async () => {
320
+ const errorResponse = {
321
+ ok: false,
322
+ status: 500,
323
+ statusText: "Error",
324
+ headers: { get: () => "application/json" },
325
+ json: () => Promise.resolve({ error: "Bulk delete failed" }),
326
+ };
327
+ vi.mocked(global.fetch).mockResolvedValue(errorResponse as any);
328
+
329
+ await expect(crudService.deleteMany(mockEndpoint, ["1"])).rejects.toThrow(
330
+ "Bulk delete failed",
331
+ );
332
+ });
333
+ });
334
+ });
@@ -0,0 +1,358 @@
1
+ import type { CrudQueryParams, CrudResponse } from "../../types";
2
+
3
+ import { crudConfig } from "../../configs";
4
+
5
+ import { buildQueryString } from "./crud-utils";
6
+ import { logger } from "../../utils";
7
+
8
+ class CrudService {
9
+ /**
10
+ * Fetch list of records
11
+ */
12
+ async fetch<T = Record<string, unknown>>(
13
+ endpoint: string,
14
+ params: CrudQueryParams,
15
+ signal?: AbortSignal,
16
+ ): Promise<CrudResponse<T>> {
17
+ const queryString = buildQueryString(params);
18
+ // Add cache buster if enabled (configure via CRUD_SERVICE_CACHE_BUSTER_ENABLED)
19
+ const cacheBuster = crudConfig.service.cacheBusterEnabled
20
+ ? `_t=${Date.now()}`
21
+ : "";
22
+ const url = queryString
23
+ ? cacheBuster
24
+ ? `${endpoint}?${queryString}&${cacheBuster}`
25
+ : `${endpoint}?${queryString}`
26
+ : cacheBuster
27
+ ? `${endpoint}?${cacheBuster}`
28
+ : endpoint;
29
+
30
+ logger.debug("Fetching from: " + url);
31
+ logger.debug("Query params:", { params });
32
+
33
+ // Configure cache based on CRUD_SERVICE_CACHE_ENABLED env variable
34
+ const fetchOptions: RequestInit = {
35
+ cache: crudConfig.service.cacheEnabled ? "default" : "no-store",
36
+ headers: crudConfig.service.cacheEnabled
37
+ ? {}
38
+ : {
39
+ "Cache-Control": "no-cache, no-store, must-revalidate",
40
+ Pragma: "no-cache",
41
+ Expires: "0",
42
+ },
43
+ };
44
+
45
+ const response = await fetch(url, { ...fetchOptions, signal });
46
+
47
+ // Check Content-Type first
48
+ const contentType = response.headers.get("content-type");
49
+ const isJSON = contentType && contentType.includes("application/json");
50
+
51
+ if (!response.ok) {
52
+ const errorText = await response.text();
53
+ logger.error("API Error", undefined, {
54
+ status: response.status,
55
+ errorText: errorText.substring(0, 200),
56
+ });
57
+ throw new Error(`Failed to fetch: ${response.statusText}`);
58
+ }
59
+
60
+ // Check Content-Type to ensure it's JSON
61
+ if (!isJSON) {
62
+ const text = await response.text();
63
+ logger.error("Invalid Content-Type", undefined, { contentType });
64
+ logger.error("Response text", undefined, {
65
+ text: text.substring(0, 200),
66
+ });
67
+ throw new Error(
68
+ `Invalid response format. Expected JSON but got ${contentType || "unknown"}. The endpoint might not exist or returned an error page.`,
69
+ );
70
+ }
71
+
72
+ const jsonData = await response.json();
73
+ logger.debug("API Response", { jsonData });
74
+
75
+ // Ensure response matches CrudResponse format
76
+ if (jsonData.items && !jsonData.data) {
77
+ // Map items to data if needed
78
+ return {
79
+ data: jsonData.items,
80
+ total: jsonData.total ?? 0,
81
+ page: jsonData.page ?? 1,
82
+ pageSize: jsonData.pageSize ?? 10,
83
+ };
84
+ }
85
+
86
+ return jsonData;
87
+ }
88
+
89
+ /**
90
+ * Create a new record
91
+ */
92
+ async create(
93
+ endpoint: string,
94
+ data: Record<string, unknown>,
95
+ ): Promise<Record<string, unknown>> {
96
+ const method = "POST";
97
+
98
+ // Debug log for user-suppliers
99
+ if (endpoint.includes("user-suppliers")) {
100
+ logger.debug("CrudService.create - Sending data", {
101
+ endpoint,
102
+ data,
103
+ userId: data.userId,
104
+ supplierId: data.supplierId,
105
+ userIdType: typeof data.userId,
106
+ supplierIdType: typeof data.supplierId,
107
+ userIdValue: data.userId,
108
+ supplierIdValue: data.supplierId,
109
+ });
110
+ }
111
+
112
+ const response = await fetch(endpoint, {
113
+ method,
114
+ headers: {
115
+ "Content-Type": "application/json",
116
+ },
117
+ body: JSON.stringify(data),
118
+ });
119
+
120
+ if (!response.ok) {
121
+ const contentType = response.headers.get("content-type");
122
+ let error: {
123
+ message?: string;
124
+ error?: string;
125
+ field?: string;
126
+ details?: unknown[];
127
+ } = { message: response.statusText };
128
+
129
+ if (contentType && contentType.includes("application/json")) {
130
+ try {
131
+ error = await response.json();
132
+ } catch {
133
+ const errorText = await response.text();
134
+ logger.error("Failed to parse error response", undefined, {
135
+ errorText: errorText.substring(0, 200),
136
+ });
137
+ throw new Error(
138
+ errorText.substring(0, 200) ||
139
+ `Failed to ${method}: ${response.statusText}`,
140
+ );
141
+ }
142
+ } else {
143
+ const errorText = await response.text();
144
+ logger.error("Non-JSON error response", undefined, {
145
+ errorText: errorText.substring(0, 200),
146
+ });
147
+ throw new Error(
148
+ errorText.substring(0, 200) ||
149
+ `Failed to ${method}: ${response.statusText}`,
150
+ );
151
+ }
152
+
153
+ const errorMessage =
154
+ error?.error ||
155
+ error?.message ||
156
+ `Failed to ${method}: ${response.statusText}`;
157
+ const apiError = new Error(errorMessage);
158
+ // Attach additional error information for better error handling
159
+ if (error.field) {
160
+ (apiError as Error & { field?: string }).field = error.field;
161
+ }
162
+ if (error.details) {
163
+ (apiError as Error & { details?: unknown[] }).details = error.details;
164
+ }
165
+ throw apiError;
166
+ }
167
+
168
+ const contentType = response.headers.get("content-type");
169
+ if (!contentType || !contentType.includes("application/json")) {
170
+ const text = await response.text();
171
+ logger.error("Invalid Content-Type", undefined, { contentType });
172
+ throw new Error(
173
+ `Invalid response format. Expected JSON but got ${contentType || "unknown"}`,
174
+ );
175
+ }
176
+
177
+ return response.json();
178
+ }
179
+
180
+ /**
181
+ * Update an existing record
182
+ */
183
+ async update(
184
+ endpoint: string,
185
+ id: string,
186
+ data: Record<string, unknown>,
187
+ ): Promise<Record<string, unknown>> {
188
+ const url = `${endpoint}/${id}`;
189
+ logger.debug("Updating record", { url, id, data });
190
+
191
+ const response = await fetch(url, {
192
+ method: "PUT",
193
+ headers: {
194
+ "Content-Type": "application/json",
195
+ },
196
+ body: JSON.stringify(data),
197
+ });
198
+
199
+ // Check Content-Type first
200
+ const contentType = response.headers.get("content-type");
201
+ const isJSON = contentType && contentType.includes("application/json");
202
+
203
+ if (!response.ok) {
204
+ let error: {
205
+ message?: string;
206
+ error?: string;
207
+ field?: string;
208
+ details?: unknown[];
209
+ } = { message: response.statusText };
210
+
211
+ if (isJSON) {
212
+ try {
213
+ error = await response.json();
214
+ } catch {
215
+ const errorText = await response.text();
216
+ logger.error("Failed to parse error response", undefined, {
217
+ errorText: errorText.substring(0, 200),
218
+ });
219
+ }
220
+ } else {
221
+ const errorText = await response.text();
222
+ logger.error("Non-JSON error response", undefined, {
223
+ status: response.status,
224
+ statusText: response.statusText,
225
+ contentType,
226
+ url: response.url,
227
+ text: errorText.substring(0, 500),
228
+ });
229
+
230
+ // Check if it's HTML (likely redirect or error page)
231
+ if (errorText.includes("<!DOCTYPE") || errorText.includes("<html")) {
232
+ throw new Error(
233
+ `API endpoint returned HTML instead of JSON. This might be a redirect to login page or an error page. Status: ${response.status}, URL: ${response.url}`,
234
+ );
235
+ }
236
+ }
237
+
238
+ const errorMessage =
239
+ error?.error ||
240
+ error?.message ||
241
+ `Failed to update: ${response.status} ${response.statusText}`;
242
+ const apiError = new Error(errorMessage);
243
+ // Attach additional error information for better error handling
244
+ if (error.field) {
245
+ (apiError as Error & { field?: string }).field = error.field;
246
+ }
247
+ if (error.details) {
248
+ (apiError as Error & { details?: unknown[] }).details = error.details;
249
+ }
250
+ throw apiError;
251
+ }
252
+
253
+ if (!isJSON) {
254
+ const text = await response.text();
255
+ logger.error("Invalid Content-Type for successful response", undefined, {
256
+ contentType,
257
+ status: response.status,
258
+ url: response.url,
259
+ text: text.substring(0, 500),
260
+ });
261
+
262
+ // Check if it's HTML
263
+ if (text.includes("<!DOCTYPE") || text.includes("<html")) {
264
+ throw new Error(
265
+ `API endpoint returned HTML instead of JSON. This might be a redirect or error page. Status: ${response.status}, URL: ${response.url}`,
266
+ );
267
+ }
268
+
269
+ throw new Error(
270
+ `Invalid response format. Expected JSON but got ${contentType || "unknown"}`,
271
+ );
272
+ }
273
+
274
+ return response.json();
275
+ }
276
+
277
+ /**
278
+ * Delete a record
279
+ */
280
+ async delete(endpoint: string, id: string): Promise<void> {
281
+ const response = await fetch(`${endpoint}/${id}`, {
282
+ method: "DELETE",
283
+ });
284
+
285
+ if (!response.ok) {
286
+ const contentType = response.headers.get("content-type");
287
+ let error: { message?: string; error?: string } = {
288
+ message: response.statusText,
289
+ };
290
+
291
+ if (contentType && contentType.includes("application/json")) {
292
+ try {
293
+ error = await response.json();
294
+ } catch {
295
+ const errorText = await response.text();
296
+ logger.error("Failed to parse error response", undefined, {
297
+ errorText: errorText.substring(0, 200),
298
+ });
299
+ }
300
+ } else {
301
+ const errorText = await response.text();
302
+ logger.error("Non-JSON error response", undefined, {
303
+ errorText: errorText.substring(0, 200),
304
+ });
305
+ }
306
+
307
+ const errorMessage =
308
+ error?.error ||
309
+ error?.message ||
310
+ `Failed to delete: ${response.statusText}`;
311
+ throw new Error(errorMessage);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Delete multiple records
317
+ */
318
+ async deleteMany(endpoint: string, ids: string[]): Promise<void> {
319
+ const response = await fetch(`${endpoint}/bulk`, {
320
+ method: "DELETE",
321
+ headers: {
322
+ "Content-Type": "application/json",
323
+ },
324
+ body: JSON.stringify({ ids }),
325
+ });
326
+
327
+ if (!response.ok) {
328
+ const contentType = response.headers.get("content-type");
329
+ let error: { message?: string; error?: string } = {
330
+ message: response.statusText,
331
+ };
332
+
333
+ if (contentType && contentType.includes("application/json")) {
334
+ try {
335
+ error = await response.json();
336
+ } catch {
337
+ const errorText = await response.text();
338
+ logger.error("Failed to parse error response", undefined, {
339
+ errorText: errorText.substring(0, 200),
340
+ });
341
+ }
342
+ } else {
343
+ const errorText = await response.text();
344
+ logger.error("Non-JSON error response", undefined, {
345
+ errorText: errorText.substring(0, 200),
346
+ });
347
+ }
348
+
349
+ const errorMessage =
350
+ error?.error ||
351
+ error?.message ||
352
+ `Failed to delete: ${response.statusText}`;
353
+ throw new Error(errorMessage);
354
+ }
355
+ }
356
+ }
357
+
358
+ export const crudService = new CrudService();