@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,109 @@
|
|
|
1
|
+
import type { EntityConfig, FieldConfig, ImportResult } from "../../types";
|
|
2
|
+
|
|
3
|
+
export function getRowValue(
|
|
4
|
+
row: Record<string, unknown>,
|
|
5
|
+
field: Pick<FieldConfig, "name" | "label">,
|
|
6
|
+
): unknown {
|
|
7
|
+
if (Object.prototype.hasOwnProperty.call(row, field.name)) {
|
|
8
|
+
return row[field.name];
|
|
9
|
+
}
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(row, field.label)) {
|
|
11
|
+
return row[field.label];
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeRowForConfig(
|
|
17
|
+
row: Record<string, unknown>,
|
|
18
|
+
config: EntityConfig,
|
|
19
|
+
): Record<string, unknown> {
|
|
20
|
+
const normalized: Record<string, unknown> = { ...row };
|
|
21
|
+
config.fields.forEach((field) => {
|
|
22
|
+
normalized[field.name] = getRowValue(row, field);
|
|
23
|
+
});
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function coerceBoolean(value: unknown): boolean | undefined {
|
|
28
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
29
|
+
if (typeof value === "boolean") return value;
|
|
30
|
+
const v = String(value).trim().toLowerCase();
|
|
31
|
+
if (["true", "1", "yes", "y", "on"].includes(v)) return true;
|
|
32
|
+
if (["false", "0", "no", "n", "off"].includes(v)) return false;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function coerceNumber(value: unknown): number | undefined {
|
|
37
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
38
|
+
const n = typeof value === "number" ? value : Number(String(value).trim());
|
|
39
|
+
if (Number.isNaN(n)) return undefined;
|
|
40
|
+
return n;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function coerceDate(value: unknown): Date | undefined {
|
|
44
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
45
|
+
if (value instanceof Date) return value;
|
|
46
|
+
const d = new Date(String(value));
|
|
47
|
+
if (Number.isNaN(d.getTime())) return undefined;
|
|
48
|
+
return d;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function coerceString(value: unknown): string | undefined {
|
|
52
|
+
if (value === undefined || value === null) return undefined;
|
|
53
|
+
const s = String(value);
|
|
54
|
+
return s === "" ? undefined : s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function coerceStringArray(value: unknown): string[] | undefined {
|
|
58
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
59
|
+
if (Array.isArray(value)) return value.map((v) => String(v)).filter(Boolean);
|
|
60
|
+
return String(value)
|
|
61
|
+
.split(",")
|
|
62
|
+
.map((v) => v.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function coerceRowForConfig(
|
|
67
|
+
row: Record<string, unknown>,
|
|
68
|
+
config: EntityConfig,
|
|
69
|
+
): Record<string, unknown> {
|
|
70
|
+
const normalized = normalizeRowForConfig(row, config);
|
|
71
|
+
const out: Record<string, unknown> = {};
|
|
72
|
+
|
|
73
|
+
config.fields.forEach((field) => {
|
|
74
|
+
if (field.hideInForm) return;
|
|
75
|
+
|
|
76
|
+
const raw = normalized[field.name];
|
|
77
|
+
|
|
78
|
+
switch (field.type) {
|
|
79
|
+
case "number":
|
|
80
|
+
case "integer":
|
|
81
|
+
out[field.name] = coerceNumber(raw);
|
|
82
|
+
break;
|
|
83
|
+
case "boolean":
|
|
84
|
+
out[field.name] = coerceBoolean(raw);
|
|
85
|
+
break;
|
|
86
|
+
case "date":
|
|
87
|
+
case "datetime":
|
|
88
|
+
out[field.name] = coerceDate(raw);
|
|
89
|
+
break;
|
|
90
|
+
case "multiselect":
|
|
91
|
+
out[field.name] = coerceStringArray(raw);
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
out[field.name] = coerceString(raw);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function emptyImportResult(): ImportResult {
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
imported: 0,
|
|
106
|
+
failed: 0,
|
|
107
|
+
errors: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { CrudQueryParams, CrudResponse } from "../../types";
|
|
2
|
+
|
|
3
|
+
import { crudConfig } from "../../configs";
|
|
4
|
+
|
|
5
|
+
import { crudService } from "./crud-service";
|
|
6
|
+
|
|
7
|
+
interface LazyLoadOptions {
|
|
8
|
+
endpoint: string;
|
|
9
|
+
initialPageSize?: number;
|
|
10
|
+
loadMoreSize?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface LazyLoadState {
|
|
14
|
+
data: unknown[];
|
|
15
|
+
total: number;
|
|
16
|
+
loadedPages: Set<number>;
|
|
17
|
+
hasMore: boolean;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class LazyLoader {
|
|
22
|
+
private state: Map<string, LazyLoadState> = new Map();
|
|
23
|
+
private lastAccessTime: Map<string, number> = new Map();
|
|
24
|
+
// Configure via CRUD_LAZY_LOADER_MAX_STATES env variable (default: 50)
|
|
25
|
+
private readonly MAX_STATES = crudConfig.lazyLoader.maxStates;
|
|
26
|
+
// Configure via CRUD_LAZY_LOADER_MAX_AGE_MS env variable (default: 600000 = 10 minutes)
|
|
27
|
+
private readonly MAX_AGE = crudConfig.lazyLoader.maxAge;
|
|
28
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
// Start cleanup interval if in browser environment
|
|
32
|
+
if (typeof window !== "undefined") {
|
|
33
|
+
this.startCleanupInterval();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start periodic cleanup of old states
|
|
39
|
+
*/
|
|
40
|
+
private startCleanupInterval(): void {
|
|
41
|
+
if (this.cleanupInterval) return;
|
|
42
|
+
|
|
43
|
+
this.cleanupInterval = setInterval(() => {
|
|
44
|
+
this.cleanupOldStates();
|
|
45
|
+
}, 300000); // Run cleanup every 5 minutes
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Stop cleanup interval
|
|
50
|
+
*/
|
|
51
|
+
private stopCleanupInterval(): void {
|
|
52
|
+
if (this.cleanupInterval) {
|
|
53
|
+
clearInterval(this.cleanupInterval);
|
|
54
|
+
this.cleanupInterval = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cleanup old and unused states
|
|
60
|
+
*/
|
|
61
|
+
private cleanupOldStates(): void {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
|
|
64
|
+
// Remove states older than MAX_AGE
|
|
65
|
+
const keysToDelete: string[] = [];
|
|
66
|
+
this.lastAccessTime.forEach((time, endpoint) => {
|
|
67
|
+
if (now - time > this.MAX_AGE) {
|
|
68
|
+
keysToDelete.push(endpoint);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
keysToDelete.forEach((endpoint) => {
|
|
73
|
+
this.state.delete(endpoint);
|
|
74
|
+
this.lastAccessTime.delete(endpoint);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// If still too many states, remove oldest ones
|
|
78
|
+
if (this.state.size > this.MAX_STATES) {
|
|
79
|
+
const sorted = Array.from(this.lastAccessTime.entries()).sort(
|
|
80
|
+
(a, b) => a[1] - b[1],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const toRemove = sorted.slice(0, this.state.size - this.MAX_STATES);
|
|
84
|
+
toRemove.forEach(([endpoint]) => {
|
|
85
|
+
this.state.delete(endpoint);
|
|
86
|
+
this.lastAccessTime.delete(endpoint);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get or initialize state for an endpoint
|
|
93
|
+
*/
|
|
94
|
+
private getState(endpoint: string): LazyLoadState {
|
|
95
|
+
// Track access time for cleanup
|
|
96
|
+
this.lastAccessTime.set(endpoint, Date.now());
|
|
97
|
+
|
|
98
|
+
if (!this.state.has(endpoint)) {
|
|
99
|
+
this.state.set(endpoint, {
|
|
100
|
+
data: [],
|
|
101
|
+
total: 0,
|
|
102
|
+
loadedPages: new Set(),
|
|
103
|
+
hasMore: true,
|
|
104
|
+
loading: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return this.state.get(endpoint)!;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load initial data
|
|
112
|
+
*/
|
|
113
|
+
async loadInitial(
|
|
114
|
+
endpoint: string,
|
|
115
|
+
queryParams: CrudQueryParams,
|
|
116
|
+
): Promise<CrudResponse> {
|
|
117
|
+
const state = this.getState(endpoint);
|
|
118
|
+
state.loading = true;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const response = await crudService.fetch(endpoint, {
|
|
122
|
+
...queryParams,
|
|
123
|
+
page: 1,
|
|
124
|
+
pageSize: queryParams.pageSize,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
state.data = response.data;
|
|
128
|
+
state.total = response.total;
|
|
129
|
+
state.loadedPages.clear();
|
|
130
|
+
state.loadedPages.add(1);
|
|
131
|
+
state.hasMore = response.data.length < response.total;
|
|
132
|
+
|
|
133
|
+
return response;
|
|
134
|
+
} finally {
|
|
135
|
+
state.loading = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load more data (next page)
|
|
141
|
+
*/
|
|
142
|
+
async loadMore(
|
|
143
|
+
endpoint: string,
|
|
144
|
+
queryParams: CrudQueryParams,
|
|
145
|
+
): Promise<CrudResponse> {
|
|
146
|
+
const state = this.getState(endpoint);
|
|
147
|
+
|
|
148
|
+
if (state.loading || !state.hasMore) {
|
|
149
|
+
return {
|
|
150
|
+
data: [],
|
|
151
|
+
total: state.total,
|
|
152
|
+
page: queryParams.page,
|
|
153
|
+
pageSize: queryParams.pageSize,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const nextPage = state.loadedPages.size + 1;
|
|
158
|
+
|
|
159
|
+
// Check if page already loaded
|
|
160
|
+
if (state.loadedPages.has(nextPage)) {
|
|
161
|
+
return {
|
|
162
|
+
data: [],
|
|
163
|
+
total: state.total,
|
|
164
|
+
page: nextPage,
|
|
165
|
+
pageSize: queryParams.pageSize,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
state.loading = true;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const response = await crudService.fetch(endpoint, {
|
|
173
|
+
...queryParams,
|
|
174
|
+
page: nextPage,
|
|
175
|
+
pageSize: queryParams.pageSize,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Merge new data with existing
|
|
179
|
+
state.data = [...state.data, ...response.data];
|
|
180
|
+
state.loadedPages.add(nextPage);
|
|
181
|
+
state.hasMore = state.data.length < state.total;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...response,
|
|
185
|
+
data: state.data,
|
|
186
|
+
};
|
|
187
|
+
} finally {
|
|
188
|
+
state.loading = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reset state for an endpoint
|
|
194
|
+
*/
|
|
195
|
+
reset(endpoint: string): void {
|
|
196
|
+
this.state.delete(endpoint);
|
|
197
|
+
this.lastAccessTime.delete(endpoint);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Cleanup all states (useful for testing or memory management)
|
|
202
|
+
*/
|
|
203
|
+
cleanupAll(): void {
|
|
204
|
+
this.state.clear();
|
|
205
|
+
this.lastAccessTime.clear();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get current cache size
|
|
210
|
+
*/
|
|
211
|
+
getCacheSize(): number {
|
|
212
|
+
return this.state.size;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if has more data to load
|
|
217
|
+
*/
|
|
218
|
+
hasMore(endpoint: string): boolean {
|
|
219
|
+
const state = this.state.get(endpoint);
|
|
220
|
+
return state?.hasMore ?? false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get current data
|
|
225
|
+
*/
|
|
226
|
+
getData(endpoint: string): unknown[] {
|
|
227
|
+
const state = this.state.get(endpoint);
|
|
228
|
+
return state?.data ?? [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get loading state
|
|
233
|
+
*/
|
|
234
|
+
isLoading(endpoint: string): boolean {
|
|
235
|
+
const state = this.state.get(endpoint);
|
|
236
|
+
return state?.loading ?? false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Singleton instance
|
|
241
|
+
export const lazyLoader = new LazyLoader();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse filters from CRUD query params
|
|
3
|
+
* Extracts filter values from filters array format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface FilterItem {
|
|
7
|
+
name: string;
|
|
8
|
+
value: unknown;
|
|
9
|
+
operator?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse filters from query params and extract specific filter values
|
|
14
|
+
* @param filtersParam - JSON string of filters array
|
|
15
|
+
* @param filterName - Name of the filter to extract
|
|
16
|
+
* @returns Filter value or undefined
|
|
17
|
+
*/
|
|
18
|
+
export function parseFilterValue(
|
|
19
|
+
filtersParam: string | null,
|
|
20
|
+
filterName: string,
|
|
21
|
+
): string | undefined {
|
|
22
|
+
if (!filtersParam) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const filters = JSON.parse(filtersParam) as FilterItem[];
|
|
28
|
+
|
|
29
|
+
const filter = filters.find((f) => f.name === filterName);
|
|
30
|
+
if (!filter) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle "in" operator - take first value from array
|
|
35
|
+
if (filter.operator === "in" && Array.isArray(filter.value)) {
|
|
36
|
+
return filter.value[0] as string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle single value
|
|
40
|
+
if (typeof filter.value === "string") {
|
|
41
|
+
return filter.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`Error parsing filter ${filterName}:`, error);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse multiple filter values from query params
|
|
53
|
+
* @param filtersParam - JSON string of filters array
|
|
54
|
+
* @param filterNames - Array of filter names to extract
|
|
55
|
+
* @returns Object with filter values
|
|
56
|
+
*/
|
|
57
|
+
export function parseFilterValues<T extends string>(
|
|
58
|
+
filtersParam: string | null,
|
|
59
|
+
filterNames: T[],
|
|
60
|
+
): Partial<Record<T, string>> {
|
|
61
|
+
if (!filtersParam) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const filters = JSON.parse(filtersParam) as FilterItem[];
|
|
67
|
+
const result: Partial<Record<T, string>> = {};
|
|
68
|
+
|
|
69
|
+
for (const filterName of filterNames) {
|
|
70
|
+
const filter = filters.find((f) => f.name === filterName);
|
|
71
|
+
if (filter) {
|
|
72
|
+
if (filter.operator === "in" && Array.isArray(filter.value)) {
|
|
73
|
+
result[filterName] = filter.value[0] as string;
|
|
74
|
+
} else if (typeof filter.value === "string") {
|
|
75
|
+
result[filterName] = filter.value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("Error parsing filters:", error);
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CrudPermissions, EntityConfig } from "../../types";
|
|
2
|
+
import type { Session } from "next-auth";
|
|
3
|
+
|
|
4
|
+
import { getCrudPermissionsFromSession } from "../../rbac/permissions";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get CRUD permissions for a user based on their session
|
|
8
|
+
* Permissions are read from session (loaded from cache in session callback)
|
|
9
|
+
* Uses memoization to return stable object references with TTL
|
|
10
|
+
*/
|
|
11
|
+
export async function getCrudPermissions(
|
|
12
|
+
session: Session | null,
|
|
13
|
+
entity: string,
|
|
14
|
+
): Promise<CrudPermissions> {
|
|
15
|
+
// Get permissions from session (already loaded in JWT)
|
|
16
|
+
const perms = getCrudPermissionsFromSession(session, entity);
|
|
17
|
+
return {
|
|
18
|
+
create: perms.create,
|
|
19
|
+
read: perms.view,
|
|
20
|
+
update: perms.update,
|
|
21
|
+
delete: perms.delete,
|
|
22
|
+
export: perms.export,
|
|
23
|
+
import: perms.import,
|
|
24
|
+
approve: perms.approve,
|
|
25
|
+
reject: perms.reject,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Merge config permissions with user permissions
|
|
31
|
+
* User permissions take precedence
|
|
32
|
+
* Config permissions are optional, so undefined means "allow if user has permission"
|
|
33
|
+
*/
|
|
34
|
+
export function mergePermissions(
|
|
35
|
+
configPermissions: EntityConfig["permissions"],
|
|
36
|
+
userPermissions: CrudPermissions,
|
|
37
|
+
): CrudPermissions {
|
|
38
|
+
if (!configPermissions) {
|
|
39
|
+
return userPermissions;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
create: (configPermissions.create ?? true) && userPermissions.create,
|
|
44
|
+
read: (configPermissions.read ?? true) && userPermissions.read,
|
|
45
|
+
update: (configPermissions.update ?? true) && userPermissions.update,
|
|
46
|
+
delete: (configPermissions.delete ?? true) && userPermissions.delete,
|
|
47
|
+
export: (configPermissions.export ?? true) && userPermissions.export,
|
|
48
|
+
import: (configPermissions.import ?? true) && userPermissions.import,
|
|
49
|
+
approve: (configPermissions.approve ?? true) && userPermissions.approve,
|
|
50
|
+
reject: (configPermissions.reject ?? true) && userPermissions.reject,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize EntityConfig for passing to Client Components
|
|
3
|
+
* Removes non-serializable React components and functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EntityConfig, FilterConfig } from '../../types'
|
|
7
|
+
|
|
8
|
+
export function serializeConfig(config: EntityConfig): Omit<EntityConfig, 'icon'> {
|
|
9
|
+
const { icon, fields, filters, ...rest } = config
|
|
10
|
+
|
|
11
|
+
if (icon && !rest.iconName) {
|
|
12
|
+
const iconComponent = icon as { displayName?: string }
|
|
13
|
+
if (iconComponent.displayName) {
|
|
14
|
+
rest.iconName = iconComponent.displayName
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const serializedFields = fields.map((field) => {
|
|
19
|
+
const {
|
|
20
|
+
renderCell: _renderCell,
|
|
21
|
+
renderForm: _renderForm,
|
|
22
|
+
renderInput: _renderInput,
|
|
23
|
+
validation: _validation,
|
|
24
|
+
dataSource,
|
|
25
|
+
...fieldRest
|
|
26
|
+
} = field
|
|
27
|
+
|
|
28
|
+
if (dataSource?.transform) {
|
|
29
|
+
const { transform: _transform, ...dataSourceRest } = dataSource
|
|
30
|
+
return {
|
|
31
|
+
...fieldRest,
|
|
32
|
+
dataSource: dataSourceRest,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return dataSource ? { ...fieldRest, dataSource } : fieldRest
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const serializedFilters = filters?.map((filter) => {
|
|
40
|
+
const { renderFilter: _renderFilter, ...filterRest } = filter
|
|
41
|
+
return filterRest as Omit<FilterConfig, 'renderFilter'>
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const serializedRowActions = rest.rowActions?.map((action) => {
|
|
45
|
+
const {
|
|
46
|
+
handler: _handler,
|
|
47
|
+
transformData: _transformData,
|
|
48
|
+
visibleWhen: _visibleWhen,
|
|
49
|
+
...actionRest
|
|
50
|
+
} = action
|
|
51
|
+
return actionRest
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...rest,
|
|
56
|
+
fields: serializedFields,
|
|
57
|
+
filters: serializedFilters,
|
|
58
|
+
rowActions: serializedRowActions,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { CrudQueryParams, CrudResponse } from "../../types";
|
|
2
|
+
|
|
3
|
+
interface StreamChunk {
|
|
4
|
+
type: "data" | "error" | "done";
|
|
5
|
+
data?: unknown[];
|
|
6
|
+
error?: string;
|
|
7
|
+
total?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Stream data from API using fetch streaming
|
|
12
|
+
*/
|
|
13
|
+
export async function* streamData(
|
|
14
|
+
endpoint: string,
|
|
15
|
+
queryParams: CrudQueryParams,
|
|
16
|
+
): AsyncGenerator<StreamChunk, void, unknown> {
|
|
17
|
+
const queryString = new URLSearchParams({
|
|
18
|
+
page: String(queryParams.page),
|
|
19
|
+
pageSize: String(queryParams.pageSize),
|
|
20
|
+
...(queryParams.search && { search: queryParams.search }),
|
|
21
|
+
...(queryParams.sort && {
|
|
22
|
+
sortField: queryParams.sort.field,
|
|
23
|
+
sortDirection: queryParams.sort.direction,
|
|
24
|
+
}),
|
|
25
|
+
...(queryParams.filters && {
|
|
26
|
+
filters: JSON.stringify(queryParams.filters),
|
|
27
|
+
}),
|
|
28
|
+
}).toString();
|
|
29
|
+
|
|
30
|
+
const url = `${endpoint}?${queryString}`;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
headers: {
|
|
35
|
+
Accept: "application/x-ndjson", // Newline-delimited JSON
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
yield {
|
|
41
|
+
type: "error",
|
|
42
|
+
error: `Failed to fetch: ${response.statusText}`,
|
|
43
|
+
};
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const reader = response.body?.getReader();
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
let buffer = "";
|
|
50
|
+
|
|
51
|
+
if (!reader) {
|
|
52
|
+
yield {
|
|
53
|
+
type: "error",
|
|
54
|
+
error: "No response body",
|
|
55
|
+
};
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
const { done, value } = await reader.read();
|
|
61
|
+
|
|
62
|
+
if (done) {
|
|
63
|
+
// Process remaining buffer
|
|
64
|
+
if (buffer.trim()) {
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(buffer);
|
|
67
|
+
yield {
|
|
68
|
+
type: "data",
|
|
69
|
+
data: Array.isArray(data) ? data : [data],
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore parse errors for last chunk
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
yield { type: "done" };
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
buffer += decoder.decode(value, { stream: true });
|
|
80
|
+
const lines = buffer.split("\n");
|
|
81
|
+
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
82
|
+
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (line.trim()) {
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(line);
|
|
87
|
+
yield {
|
|
88
|
+
type: "data",
|
|
89
|
+
data: Array.isArray(data) ? data : [data],
|
|
90
|
+
total: data.total,
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
yield {
|
|
94
|
+
type: "error",
|
|
95
|
+
error: `Parse error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
yield {
|
|
103
|
+
type: "error",
|
|
104
|
+
error: error instanceof Error ? error.message : "Stream failed",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load data progressively from stream
|
|
111
|
+
*/
|
|
112
|
+
export async function loadStreamData(
|
|
113
|
+
endpoint: string,
|
|
114
|
+
queryParams: CrudQueryParams,
|
|
115
|
+
onChunk: (chunk: unknown[]) => void,
|
|
116
|
+
): Promise<CrudResponse> {
|
|
117
|
+
const allData: unknown[] = [];
|
|
118
|
+
let total = 0;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
for await (const chunk of streamData(endpoint, queryParams)) {
|
|
122
|
+
if (chunk.type === "data" && chunk.data) {
|
|
123
|
+
allData.push(...chunk.data);
|
|
124
|
+
if (chunk.total) {
|
|
125
|
+
total = chunk.total;
|
|
126
|
+
}
|
|
127
|
+
onChunk(chunk.data);
|
|
128
|
+
} else if (chunk.type === "error") {
|
|
129
|
+
throw new Error(chunk.error);
|
|
130
|
+
} else if (chunk.type === "done") {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
data: allData,
|
|
137
|
+
total,
|
|
138
|
+
page: queryParams.page,
|
|
139
|
+
pageSize: queryParams.pageSize,
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error("Stream loading error:", error);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|