@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.
- package/package.json +2 -1
- package/src/assets/erp_wallpaper.png +0 -0
- package/src/assets/goeat_logo.png +0 -0
- package/src/audit/audit-manager.ts +139 -0
- package/src/audit/index.ts +11 -0
- package/src/audit/memory-audit-logger.ts +86 -0
- package/src/audit/types.ts +50 -0
- package/src/auth/auth-service.ts +97 -0
- package/src/auth/index.ts +266 -0
- package/src/code-generation/index.ts +69 -0
- package/src/configs/auth-routes.ts +17 -0
- package/src/configs/crud.ts +136 -0
- package/src/configs/data/navigations.ts +781 -0
- package/src/configs/data/oauth-links.ts +10 -0
- package/src/configs/entities/material-categories.config.ts +125 -0
- package/src/configs/i18n.ts +12 -0
- package/src/configs/index.ts +26 -0
- package/src/configs/status.ts +25 -0
- package/src/configs/themes.ts +100 -0
- package/src/crud/components/crud-bulk-actions.tsx +91 -0
- package/src/crud/components/crud-card-view.tsx +241 -0
- package/src/crud/components/crud-context.tsx +122 -0
- package/src/crud/components/crud-delete-dialog.tsx +145 -0
- package/src/crud/components/crud-dialog.tsx +406 -0
- package/src/crud/components/crud-empty-state.tsx +104 -0
- package/src/crud/components/crud-export-button.tsx +170 -0
- package/src/crud/components/crud-field-renderer.tsx +653 -0
- package/src/crud/components/crud-filter-chips.tsx +102 -0
- package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
- package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
- package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
- package/src/crud/components/crud-filters/index.tsx +76 -0
- package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
- package/src/crud/components/crud-filters/select-filter.tsx +141 -0
- package/src/crud/components/crud-filters/text-filter.tsx +86 -0
- package/src/crud/components/crud-form.tsx +642 -0
- package/src/crud/components/crud-import-dialog.tsx +440 -0
- package/src/crud/components/crud-infinite-scroll.tsx +116 -0
- package/src/crud/components/crud-page.tsx +1017 -0
- package/src/crud/components/crud-provider.tsx +277 -0
- package/src/crud/components/crud-row-actions.tsx +189 -0
- package/src/crud/components/crud-search.tsx +82 -0
- package/src/crud/components/crud-sheet.tsx +336 -0
- package/src/crud/components/crud-table-skeleton.tsx +26 -0
- package/src/crud/components/crud-table-toolbar.tsx +91 -0
- package/src/crud/components/crud-table.tsx +352 -0
- package/src/crud/components/crud-virtual-table.tsx +55 -0
- package/src/crud/components/index.tsx +20 -0
- package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
- package/src/crud/crud-filters/datetime-filter.tsx +82 -0
- package/src/crud/crud-filters/filter-builder.tsx +64 -0
- package/src/crud/crud-filters/index.tsx +78 -0
- package/src/crud/crud-filters/radio-filter.tsx +79 -0
- package/src/crud/crud-filters/select-filter.tsx +148 -0
- package/src/crud/crud-filters/text-filter.tsx +81 -0
- package/src/crud/index.ts +43 -0
- package/src/crud/lib/crud-service.test.ts +334 -0
- package/src/crud/lib/crud-service.ts +358 -0
- package/src/crud/lib/crud-utils.test.ts +354 -0
- package/src/crud/lib/crud-utils.ts +299 -0
- package/src/crud/lib/crud-validator.ts +247 -0
- package/src/crud/lib/data-loader.ts +234 -0
- package/src/crud/lib/field-calculator.ts +241 -0
- package/src/crud/lib/field-formatter.ts +240 -0
- package/src/crud/lib/import-export-service.test.ts +290 -0
- package/src/crud/lib/import-export-service.ts +352 -0
- package/src/crud/lib/import-server-utils.ts +109 -0
- package/src/crud/lib/lazy-loader.ts +241 -0
- package/src/crud/lib/parse-filters.ts +85 -0
- package/src/crud/lib/permissions.ts +52 -0
- package/src/crud/lib/serialize-config.ts +60 -0
- package/src/crud/lib/stream-loader.ts +145 -0
- package/src/crud/lib/translate-config.ts +335 -0
- package/src/crud/lib/types.ts +11 -0
- package/src/crud/pages/entity-crud-page.tsx +144 -0
- package/src/crud/server.ts +8 -0
- package/src/home/constants.tsx +142 -0
- package/src/home/feature-showcase.tsx +171 -0
- package/src/home/home-page.tsx +191 -0
- package/src/home/hooks/index.ts +1 -0
- package/src/home/hooks/useWidgetPreferences.ts +167 -0
- package/src/home/index.ts +33 -0
- package/src/home/quick-access-dialog.tsx +271 -0
- package/src/home/quick-access-menu.tsx +267 -0
- package/src/home/types.ts +140 -0
- package/src/home/welcome-card.tsx +92 -0
- package/src/home/widget-container.tsx +258 -0
- package/src/home/widgets/base-widget.tsx +200 -0
- package/src/home/widgets/customers-widget.tsx +74 -0
- package/src/home/widgets/index.ts +6 -0
- package/src/home/widgets/orders-widget.tsx +87 -0
- package/src/home/widgets/revenue-widget.tsx +71 -0
- package/src/home/widgets/stock-widget.tsx +109 -0
- package/src/hooks/index.tsx +598 -0
- package/src/hooks/use-tenant.test.tsx +30 -0
- package/src/hooks/use-tenant.ts +5 -0
- package/src/index.ts +17 -0
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
- package/src/infrastructure/api-service.ts +317 -0
- package/src/infrastructure/cache/cache-manager.ts +107 -0
- package/src/infrastructure/cache/cache.ts +120 -0
- package/src/infrastructure/cache/index.ts +8 -0
- package/src/infrastructure/cache/types.ts +48 -0
- package/src/infrastructure/cron/cron-manager.ts +239 -0
- package/src/infrastructure/cron/index.ts +6 -0
- package/src/infrastructure/cron/types.ts +41 -0
- package/src/infrastructure/event-bus/event-bus.ts +145 -0
- package/src/infrastructure/event-bus/index.ts +2 -0
- package/src/infrastructure/event-bus/types.ts +22 -0
- package/src/infrastructure/index.ts +32 -0
- package/src/infrastructure/lock/decorators.ts +67 -0
- package/src/infrastructure/lock/index.ts +2 -0
- package/src/infrastructure/lock/lock-manager.ts +33 -0
- package/src/infrastructure/logger/index.ts +2 -0
- package/src/infrastructure/logger/logger.ts +96 -0
- package/src/infrastructure/logger/types.ts +25 -0
- package/src/layout/index.tsx +185 -0
- package/src/navigation/index.ts +91 -0
- package/src/notification/index.ts +14 -0
- package/src/notification/notification-service.ts +120 -0
- package/src/notification/storage/in-memory.ts +56 -0
- package/src/notification/storage/index.ts +1 -0
- package/src/notification/types.ts +51 -0
- package/src/organization/branch-service.ts +299 -0
- package/src/organization/branches.config.ts +154 -0
- package/src/organization/index.ts +5 -0
- package/src/plugin/apps-registry.ts +97 -0
- package/src/plugin/index.ts +5 -0
- package/src/plugin/types.ts +41 -0
- package/src/providers/index.tsx +109 -0
- package/src/providers/tenant-provider.tsx +45 -0
- package/src/rbac/components/roles/role-card.tsx +158 -0
- package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
- package/src/rbac/components/roles/role-toolbar.tsx +123 -0
- package/src/rbac/hooks/use-role-operations.ts +159 -0
- package/src/rbac/hooks/use-roles-data.ts +59 -0
- package/src/rbac/index.ts +297 -0
- package/src/rbac/lib/permission-helpers.ts +63 -0
- package/src/rbac/pages/action-list-page.tsx +25 -0
- package/src/rbac/pages/resource-list-page.tsx +25 -0
- package/src/rbac/pages/role-list-page.tsx +378 -0
- package/src/rbac/permission-service.ts +140 -0
- package/src/rbac/permissions.ts +135 -0
- package/src/rbac/resource-service.ts +115 -0
- package/src/rbac/resource-validator.ts +119 -0
- package/src/rbac/role-service.ts +165 -0
- package/src/rbac/server.ts +16 -0
- package/src/rbac/types.ts +38 -0
- package/src/schemas/action.schema.ts +66 -0
- package/src/schemas/branch.schema.ts +52 -0
- package/src/schemas/coming-soon-schema.ts +9 -0
- package/src/schemas/company.schema.ts +44 -0
- package/src/schemas/forgot-passward-schema.ts +9 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/material-category.schema.ts +43 -0
- package/src/schemas/material-pricing.schema.ts +74 -0
- package/src/schemas/material.schema.ts +76 -0
- package/src/schemas/materials.ts +52 -0
- package/src/schemas/new-passward-schema.ts +15 -0
- package/src/schemas/partner-company.schema.ts +149 -0
- package/src/schemas/register-schema.ts +36 -0
- package/src/schemas/resource.schema.ts +133 -0
- package/src/schemas/role.schema.ts +11 -0
- package/src/schemas/sign-in-schema.ts +24 -0
- package/src/schemas/supplier-pricing.schema.ts +15 -0
- package/src/schemas/supplier.schema.ts +120 -0
- package/src/schemas/system-category-group.schema.ts +67 -0
- package/src/schemas/system-category.schema.ts +77 -0
- package/src/schemas/system-config.schema.ts +118 -0
- package/src/schemas/uom.schema.ts +75 -0
- package/src/schemas/user-supplier.schema.ts +179 -0
- package/src/schemas/user.schema.ts +18 -0
- package/src/schemas/verify-email-schema.ts +9 -0
- package/src/schemas/warehouse.schema.ts +49 -0
- package/src/system/components/categories/category-list.tsx +529 -0
- package/src/system/components/categories/category-manager.tsx +89 -0
- package/src/system/components/categories/group-sidebar.tsx +308 -0
- package/src/system/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/components/settings/setting-field.tsx +291 -0
- package/src/system/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/components/settings/settings-groups.ts +80 -0
- package/src/system/components/settings/settings-search.tsx +71 -0
- package/src/system/components/settings/settings-section.tsx +74 -0
- package/src/system/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/constants.ts +3 -0
- package/src/system/index.ts +150 -0
- package/src/system/job-manager.ts +176 -0
- package/src/system/pages/components/categories/category-list.tsx +537 -0
- package/src/system/pages/components/categories/category-manager.tsx +90 -0
- package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
- package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
- package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/pages/components/settings/setting-field.tsx +292 -0
- package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/pages/components/settings/settings-groups.ts +87 -0
- package/src/system/pages/components/settings/settings-page.tsx +372 -0
- package/src/system/pages/components/settings/settings-search.tsx +71 -0
- package/src/system/pages/components/settings/settings-section.tsx +74 -0
- package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/pages/components/settings/system-settings.tsx +244 -0
- package/src/system/pages/system-category-page.tsx +15 -0
- package/src/system/pages/system-settings-page.tsx +380 -0
- package/src/system/schemas/system-category-group.schema.ts +46 -0
- package/src/system/schemas/system-category.schema.ts +56 -0
- package/src/system/services/settings-service.ts +127 -0
- package/src/system/services/system-category-service.ts +63 -0
- package/src/system/types.ts +45 -0
- package/src/types/index.ts +703 -0
- package/src/ui/auth/auth-layout.tsx +135 -0
- package/src/ui/auth/forgot-password-form.tsx +98 -0
- package/src/ui/auth/index.tsx +7 -0
- package/src/ui/auth/new-password-form.tsx +107 -0
- package/src/ui/auth/oauth-links.tsx +30 -0
- package/src/ui/auth/register-form.tsx +202 -0
- package/src/ui/auth/sign-in-form.tsx +238 -0
- package/src/ui/auth/verify-email-form.tsx +104 -0
- package/src/ui/crud/index.tsx +10 -0
- package/src/ui/data-display/accordion.tsx +65 -0
- package/src/ui/data-display/aspect-ratio.tsx +11 -0
- package/src/ui/data-display/avatar.tsx +163 -0
- package/src/ui/data-display/bento-grid.tsx +77 -0
- package/src/ui/data-display/carousel.tsx +249 -0
- package/src/ui/data-display/chart.tsx +363 -0
- package/src/ui/data-display/code-block-highlight.tsx +54 -0
- package/src/ui/data-display/collapsible.tsx +42 -0
- package/src/ui/data-display/compact-stat-bar.tsx +149 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
- package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
- package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
- package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
- package/src/ui/data-display/data-table/data-table.tsx +597 -0
- package/src/ui/data-display/data-table/index.ts +44 -0
- package/src/ui/data-display/data-table-column-header.tsx +75 -0
- package/src/ui/data-display/data-table-pagination.tsx +130 -0
- package/src/ui/data-display/data-table-view-options.tsx +59 -0
- package/src/ui/data-display/formatted-number-input.tsx +210 -0
- package/src/ui/data-display/highlight.tsx +20 -0
- package/src/ui/data-display/hover-card.tsx +48 -0
- package/src/ui/data-display/index.tsx +50 -0
- package/src/ui/data-display/iphone-15-pro.tsx +114 -0
- package/src/ui/data-display/kanban/index.ts +4 -0
- package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
- package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
- package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
- package/src/ui/data-display/kanban/kanban-types.ts +21 -0
- package/src/ui/data-display/kpi-card.tsx +68 -0
- package/src/ui/data-display/media-grid.tsx +110 -0
- package/src/ui/data-display/safari.tsx +175 -0
- package/src/ui/data-display/show-more-text.tsx +55 -0
- package/src/ui/data-display/tabs.tsx +68 -0
- package/src/ui/data-display/timeline.tsx +256 -0
- package/src/ui/feedback/alert.tsx +60 -0
- package/src/ui/feedback/context-menu.tsx +245 -0
- package/src/ui/feedback/drawer.tsx +132 -0
- package/src/ui/feedback/error-dialog.tsx +273 -0
- package/src/ui/feedback/index.tsx +183 -0
- package/src/ui/feedback/progress.tsx +32 -0
- package/src/ui/feedback/sheet.tsx +148 -0
- package/src/ui/feedback/sonner.tsx +36 -0
- package/src/ui/forms/command.tsx +157 -0
- package/src/ui/forms/date-picker.tsx +73 -0
- package/src/ui/forms/date-range-picker.tsx +76 -0
- package/src/ui/forms/date-time-picker.tsx +109 -0
- package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
- package/src/ui/forms/editor/index.tsx +130 -0
- package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
- package/src/ui/forms/emoji-picker.tsx +109 -0
- package/src/ui/forms/file-dropzone.tsx +169 -0
- package/src/ui/forms/file-thumbnail.tsx +29 -0
- package/src/ui/forms/index.tsx +201 -0
- package/src/ui/forms/input-file.tsx +99 -0
- package/src/ui/forms/input-group.tsx +46 -0
- package/src/ui/forms/input-otp.tsx +81 -0
- package/src/ui/forms/input-phone.tsx +172 -0
- package/src/ui/forms/input-spin.tsx +116 -0
- package/src/ui/forms/input-tags.tsx +219 -0
- package/src/ui/forms/input-time.tsx +42 -0
- package/src/ui/forms/multi-select.tsx +629 -0
- package/src/ui/forms/multiple-date-picker.tsx +74 -0
- package/src/ui/forms/radio-group.tsx +42 -0
- package/src/ui/forms/rating.tsx +158 -0
- package/src/ui/forms/time-picker.tsx +57 -0
- package/src/ui/index.tsx +17 -0
- package/src/ui/layout/animated-list.tsx +77 -0
- package/src/ui/layout/animated-sidebar.tsx +294 -0
- package/src/ui/layout/command-menu.tsx +355 -0
- package/src/ui/layout/customizer.tsx +324 -0
- package/src/ui/layout/footer.tsx +43 -0
- package/src/ui/layout/full-screen-toggle.tsx +52 -0
- package/src/ui/layout/header-breadcrumb.tsx +77 -0
- package/src/ui/layout/horizontal-layout-header.tsx +83 -0
- package/src/ui/layout/horizontal-layout.tsx +50 -0
- package/src/ui/layout/index.tsx +25 -0
- package/src/ui/layout/language-dropdown.tsx +103 -0
- package/src/ui/layout/logo.tsx +63 -0
- package/src/ui/layout/main-layout.tsx +57 -0
- package/src/ui/layout/mode-dropdown.tsx +58 -0
- package/src/ui/layout/notification-dropdown.tsx +127 -0
- package/src/ui/layout/page-tabs.tsx +306 -0
- package/src/ui/layout/route-cache.tsx +214 -0
- package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
- package/src/ui/layout/sidebar.tsx +279 -0
- package/src/ui/layout/tab-content-cache.tsx +201 -0
- package/src/ui/layout/tab-navigation-provider.tsx +536 -0
- package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
- package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
- package/src/ui/layout/user-dropdown.tsx +188 -0
- package/src/ui/layout/vertical-layout-header.tsx +65 -0
- package/src/ui/layout/vertical-layout.tsx +47 -0
- package/src/ui/management/audit-log-page.tsx +209 -0
- package/src/ui/management/cache-management.tsx +349 -0
- package/src/ui/management/index.ts +3 -0
- package/src/ui/management/job-management.tsx +308 -0
- package/src/ui/pages/not-found.tsx +30 -0
- package/src/ui/primitives/badge.tsx +66 -0
- package/src/ui/primitives/breadcrumb.tsx +103 -0
- package/src/ui/primitives/button.tsx +129 -0
- package/src/ui/primitives/calendar.tsx +74 -0
- package/src/ui/primitives/card.tsx +86 -0
- package/src/ui/primitives/checkbox.tsx +31 -0
- package/src/ui/primitives/client.ts +30 -0
- package/src/ui/primitives/combobox.tsx +290 -0
- package/src/ui/primitives/dialog.tsx +121 -0
- package/src/ui/primitives/dropdown-menu.tsx +239 -0
- package/src/ui/primitives/dynamic-icon.tsx +24 -0
- package/src/ui/primitives/index.tsx +134 -0
- package/src/ui/primitives/input-number.tsx +131 -0
- package/src/ui/primitives/input.tsx +22 -0
- package/src/ui/primitives/keyboard.tsx +23 -0
- package/src/ui/primitives/label.tsx +24 -0
- package/src/ui/primitives/menubar.tsx +262 -0
- package/src/ui/primitives/navigation-menu.tsx +157 -0
- package/src/ui/primitives/pagination.tsx +118 -0
- package/src/ui/primitives/popover.tsx +56 -0
- package/src/ui/primitives/prefetch-link.tsx +60 -0
- package/src/ui/primitives/resizable.tsx +59 -0
- package/src/ui/primitives/scroll-area.tsx +63 -0
- package/src/ui/primitives/select.tsx +172 -0
- package/src/ui/primitives/separator.tsx +51 -0
- package/src/ui/primitives/sidebar.tsx +844 -0
- package/src/ui/primitives/slider.tsx +27 -0
- package/src/ui/primitives/status-badge.tsx +47 -0
- package/src/ui/primitives/sticky-layout.tsx +50 -0
- package/src/ui/primitives/switch.tsx +29 -0
- package/src/ui/primitives/table.tsx +116 -0
- package/src/ui/primitives/tabs.tsx +55 -0
- package/src/ui/primitives/toggle-group.tsx +70 -0
- package/src/ui/primitives/toggle.tsx +47 -0
- package/src/ui/primitives/tooltip.tsx +59 -0
- package/src/user/components/dangerous-zone.tsx +34 -0
- package/src/user/components/delete-account-form.tsx +40 -0
- package/src/user/components/index.ts +4 -0
- package/src/user/components/profile-info-form.tsx +390 -0
- package/src/user/components/profile-info.tsx +32 -0
- package/src/user/components/unified-profile-dialog.tsx +1019 -0
- package/src/user/components/user-stats.tsx +27 -0
- package/src/user/components/user-toolbar.tsx +137 -0
- package/src/user/components/users-card-view.tsx +253 -0
- package/src/user/index.ts +11 -0
- package/src/user/pages/user-list-page.tsx +234 -0
- package/src/user/pages/users-client-page.tsx +385 -0
- package/src/user/profile-page.tsx +19 -0
- package/src/user/schemas.ts +68 -0
- package/src/user/types.ts +34 -0
- package/src/user/user-service.ts +538 -0
- package/src/utils/index.ts +906 -0
- package/src/workflow/activity-timeline.tsx +412 -0
- package/src/workflow/approval-workflow.tsx +31 -0
- 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();
|