@quazardous/qdadm 1.19.3
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/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- package/src/zones/zones.ts +191 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for qdadm framework
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============ ENTITY TYPES ============
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base interface for all entity records
|
|
9
|
+
*/
|
|
10
|
+
export interface EntityRecord {
|
|
11
|
+
id: string | number
|
|
12
|
+
[key: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parameters for list operations
|
|
17
|
+
*/
|
|
18
|
+
export interface ListParams {
|
|
19
|
+
page?: number
|
|
20
|
+
page_size?: number
|
|
21
|
+
search?: string
|
|
22
|
+
filters?: Record<string, unknown>
|
|
23
|
+
sort_by?: string
|
|
24
|
+
sort_order?: 'asc' | 'desc'
|
|
25
|
+
cacheSafe?: boolean
|
|
26
|
+
[key: string]: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Result of list operations
|
|
31
|
+
*/
|
|
32
|
+
export interface ListResult<T = EntityRecord> {
|
|
33
|
+
items: T[]
|
|
34
|
+
total: number
|
|
35
|
+
fromCache?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============ STORAGE TYPES ============
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Storage capabilities declaration
|
|
42
|
+
*/
|
|
43
|
+
export interface StorageCapabilities {
|
|
44
|
+
supportsTotal: boolean
|
|
45
|
+
supportsFilters: boolean
|
|
46
|
+
supportsPagination: boolean
|
|
47
|
+
supportsCaching: boolean
|
|
48
|
+
requiresAuth?: boolean
|
|
49
|
+
searchFields?: string[]
|
|
50
|
+
/** Cache TTL in milliseconds (0=disabled, -1=infinite, >0=TTL). Can be set dynamically from API headers. */
|
|
51
|
+
cacheTtlMs?: number
|
|
52
|
+
/** Asymmetric mode: list() and get() return different structures. Falls back from EntityManagerOptions. */
|
|
53
|
+
asymmetric?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============ FIELD TYPES ============
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Field type definitions
|
|
60
|
+
*/
|
|
61
|
+
export type FieldType =
|
|
62
|
+
| 'text'
|
|
63
|
+
| 'number'
|
|
64
|
+
| 'boolean'
|
|
65
|
+
| 'select'
|
|
66
|
+
| 'multiselect'
|
|
67
|
+
| 'date'
|
|
68
|
+
| 'datetime'
|
|
69
|
+
| 'email'
|
|
70
|
+
| 'url'
|
|
71
|
+
| 'textarea'
|
|
72
|
+
| 'password'
|
|
73
|
+
| 'array'
|
|
74
|
+
| 'object'
|
|
75
|
+
| 'json'
|
|
76
|
+
| string
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Field configuration
|
|
80
|
+
*/
|
|
81
|
+
export interface FieldConfig {
|
|
82
|
+
type: FieldType
|
|
83
|
+
label?: string
|
|
84
|
+
required?: boolean
|
|
85
|
+
default?: unknown | (() => unknown)
|
|
86
|
+
options?: Array<{ label: string; value: unknown }>
|
|
87
|
+
reference?: {
|
|
88
|
+
entity: string
|
|
89
|
+
labelField?: string
|
|
90
|
+
valueField?: string
|
|
91
|
+
}
|
|
92
|
+
listable?: boolean
|
|
93
|
+
editable?: boolean
|
|
94
|
+
sortable?: boolean
|
|
95
|
+
filterable?: boolean
|
|
96
|
+
searchable?: boolean
|
|
97
|
+
validator?: (value: unknown) => boolean | string
|
|
98
|
+
[key: string]: unknown
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============ ENTITY MANAGER TYPES ============
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Child entity configuration
|
|
105
|
+
*/
|
|
106
|
+
export interface ChildConfig {
|
|
107
|
+
entity: string
|
|
108
|
+
endpoint?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parent entity configuration
|
|
113
|
+
*/
|
|
114
|
+
export interface ParentConfig {
|
|
115
|
+
entity: string
|
|
116
|
+
foreignKey: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Relation configuration
|
|
121
|
+
*/
|
|
122
|
+
export interface RelationConfig {
|
|
123
|
+
entity: string
|
|
124
|
+
through?: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Navigation configuration for auto-generated menus
|
|
129
|
+
*/
|
|
130
|
+
export interface NavConfig {
|
|
131
|
+
icon?: string
|
|
132
|
+
section?: string
|
|
133
|
+
weight?: number
|
|
134
|
+
visible?: boolean | (() => boolean)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* EntityManager constructor options
|
|
139
|
+
*/
|
|
140
|
+
export interface EntityManagerOptions<T extends EntityRecord = EntityRecord> {
|
|
141
|
+
name: string
|
|
142
|
+
storage?: IStorage<T> | null
|
|
143
|
+
idField?: string
|
|
144
|
+
label?: string
|
|
145
|
+
labelPlural?: string
|
|
146
|
+
routePrefix?: string
|
|
147
|
+
labelField?: string | ((entity: T) => string)
|
|
148
|
+
fields?: Record<string, FieldConfig>
|
|
149
|
+
localFilterThreshold?: number | null
|
|
150
|
+
/** Cache TTL in milliseconds (0=disabled, -1=infinite, >0=TTL). Overrides global, overridden by storage. */
|
|
151
|
+
cacheTtlMs?: number | null
|
|
152
|
+
/** Asymmetric mode: list() and get() return different structures. get() skips list cache. */
|
|
153
|
+
asymmetric?: boolean
|
|
154
|
+
/** Detail cache TTL in milliseconds (0=disabled (default), -1=infinite, >0=TTL). Only used when asymmetric=true. */
|
|
155
|
+
detailCacheTtlMs?: number
|
|
156
|
+
readOnly?: boolean
|
|
157
|
+
warmup?: boolean
|
|
158
|
+
authSensitive?: boolean
|
|
159
|
+
system?: boolean
|
|
160
|
+
scopeWhitelist?: string[] | null
|
|
161
|
+
isOwn?: ((record: T, user: unknown) => boolean) | null
|
|
162
|
+
children?: Record<string, ChildConfig>
|
|
163
|
+
parent?: ParentConfig | null
|
|
164
|
+
parents?: Record<string, ParentConfig>
|
|
165
|
+
relations?: Record<string, RelationConfig>
|
|
166
|
+
authAdapter?: EntityAuthAdapter | null
|
|
167
|
+
nav?: NavConfig
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============ AUTH TYPES ============
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Standard auth actions
|
|
174
|
+
*/
|
|
175
|
+
export type AuthAction = 'read' | 'list' | 'create' | 'update' | 'delete'
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Entity auth adapter interface
|
|
179
|
+
*/
|
|
180
|
+
export interface EntityAuthAdapter {
|
|
181
|
+
canPerform(entity: string, action: AuthAction): boolean
|
|
182
|
+
canAccessRecord?(entity: string, record: unknown): boolean
|
|
183
|
+
isGranted?(permission: string, record?: unknown): boolean
|
|
184
|
+
getCurrentUser?(): unknown
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============ SIGNAL TYPES ============
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Signal handler context
|
|
191
|
+
*/
|
|
192
|
+
export interface ListenerContext {
|
|
193
|
+
id?: string
|
|
194
|
+
cancel: () => void
|
|
195
|
+
emit: (signal: string, payload: unknown) => Promise<void>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Signal event object
|
|
200
|
+
*/
|
|
201
|
+
export interface SignalEvent {
|
|
202
|
+
name: string
|
|
203
|
+
data: unknown
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Signal handler function
|
|
208
|
+
*/
|
|
209
|
+
export type SignalHandler = (
|
|
210
|
+
event: SignalEvent,
|
|
211
|
+
ctx: ListenerContext
|
|
212
|
+
) => void | Promise<void>
|
|
213
|
+
|
|
214
|
+
// ============ HOOK TYPES ============
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Hook registration options
|
|
218
|
+
*/
|
|
219
|
+
export interface HookOptions {
|
|
220
|
+
priority?: number
|
|
221
|
+
id?: string
|
|
222
|
+
after?: string | string[]
|
|
223
|
+
once?: boolean
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Hook event object
|
|
228
|
+
*/
|
|
229
|
+
export interface HookEvent {
|
|
230
|
+
name: string
|
|
231
|
+
data: unknown
|
|
232
|
+
cancel: () => void
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Hook handler function
|
|
237
|
+
*/
|
|
238
|
+
export type HookHandler = (event: HookEvent) => void | Promise<void>
|
|
239
|
+
|
|
240
|
+
// ============ PERMISSION TYPES ============
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Permission definition
|
|
244
|
+
*/
|
|
245
|
+
export interface PermissionDefinition {
|
|
246
|
+
key: string
|
|
247
|
+
namespace: string
|
|
248
|
+
action: string
|
|
249
|
+
module: string | null
|
|
250
|
+
label: string
|
|
251
|
+
description: string | null
|
|
252
|
+
custom: boolean
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============ FORWARD DECLARATIONS ============
|
|
256
|
+
|
|
257
|
+
// These will be properly typed when their modules are converted
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* IStorage interface (forward declaration)
|
|
261
|
+
*/
|
|
262
|
+
export interface IStorage<T extends EntityRecord = EntityRecord> {
|
|
263
|
+
list(params?: ListParams, context?: unknown): Promise<ListResult<T>>
|
|
264
|
+
get(id: string | number, context?: unknown): Promise<T | null>
|
|
265
|
+
create(data: Partial<T>): Promise<T>
|
|
266
|
+
update(id: string | number, data: Partial<T>): Promise<T>
|
|
267
|
+
patch?(id: string | number, data: Partial<T>): Promise<T>
|
|
268
|
+
delete(id: string | number): Promise<void>
|
|
269
|
+
request?(
|
|
270
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
271
|
+
endpoint: string,
|
|
272
|
+
options?: RequestOptions
|
|
273
|
+
): Promise<unknown>
|
|
274
|
+
reset?(): void
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Request options for storage
|
|
279
|
+
*/
|
|
280
|
+
export interface RequestOptions {
|
|
281
|
+
data?: unknown
|
|
282
|
+
params?: Record<string, unknown>
|
|
283
|
+
headers?: Record<string, string>
|
|
284
|
+
context?: unknown
|
|
285
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for pluralize module
|
|
3
|
+
*/
|
|
4
|
+
declare module 'pluralize' {
|
|
5
|
+
function pluralize(word: string, count?: number, inclusive?: boolean): string
|
|
6
|
+
|
|
7
|
+
namespace pluralize {
|
|
8
|
+
function plural(word: string): string
|
|
9
|
+
function singular(word: string): string
|
|
10
|
+
function isPlural(word: string): boolean
|
|
11
|
+
function isSingular(word: string): boolean
|
|
12
|
+
function addPluralRule(rule: string | RegExp, replacement: string): void
|
|
13
|
+
function addSingularRule(rule: string | RegExp, replacement: string): void
|
|
14
|
+
function addIrregularRule(single: string, plural: string): void
|
|
15
|
+
function addUncountableRule(word: string | RegExp): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export = pluralize
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for @quazardous/quarkernel
|
|
3
|
+
*
|
|
4
|
+
* quarkernel is an event-driven kernel library used by qdadm
|
|
5
|
+
* for signal/event handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare module '@quazardous/quarkernel' {
|
|
9
|
+
export interface KernelOptions {
|
|
10
|
+
debug?: boolean
|
|
11
|
+
[key: string]: unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ListenerOptions {
|
|
15
|
+
priority?: number
|
|
16
|
+
id?: string
|
|
17
|
+
after?: string | string[]
|
|
18
|
+
once?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ListenerContext {
|
|
22
|
+
id?: string
|
|
23
|
+
cancel: () => void
|
|
24
|
+
emit: (signal: string, payload: unknown) => Promise<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ListenerCallback = (
|
|
28
|
+
event: { name: string; data: unknown },
|
|
29
|
+
ctx: ListenerContext
|
|
30
|
+
) => void | Promise<void>
|
|
31
|
+
|
|
32
|
+
export interface ExecutionError {
|
|
33
|
+
listenerId: string
|
|
34
|
+
error: Error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface QuarKernel {
|
|
38
|
+
on(signal: string, handler: ListenerCallback, options?: ListenerOptions): () => void
|
|
39
|
+
once(signal: string, handler?: ListenerCallback, options?: ListenerOptions): () => void
|
|
40
|
+
off(signal: string, handler: ListenerCallback): void
|
|
41
|
+
emit(signal: string, payload?: unknown): Promise<void>
|
|
42
|
+
emitSerial(signal: string, payload?: unknown): Promise<void>
|
|
43
|
+
clearExecutionErrors(): void
|
|
44
|
+
getExecutionErrors(): ExecutionError[]
|
|
45
|
+
debug(enabled: boolean): void
|
|
46
|
+
destroy(): void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createKernel(options?: KernelOptions): QuarKernel
|
|
50
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { formatDate, formatDateTime, formatDuration } from '@/dashboard/utils/formatters'
|
|
6
|
+
*
|
|
7
|
+
* {{ formatDate(item.created_at) }}
|
|
8
|
+
* {{ formatDateTime(item.updated_at) }}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// DATE/TIME FORMATTING
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format a date string to localized date+time
|
|
17
|
+
* Default format: toLocaleString() (e.g., "20/12/2025, 14:30:00")
|
|
18
|
+
*
|
|
19
|
+
* @param dateStr - ISO date string or Date object
|
|
20
|
+
* @param options - Intl.DateTimeFormat options
|
|
21
|
+
* @returns Formatted date string or '-' if empty
|
|
22
|
+
*/
|
|
23
|
+
export function formatDate(
|
|
24
|
+
dateStr: string | Date | null | undefined,
|
|
25
|
+
options: Intl.DateTimeFormatOptions = {}
|
|
26
|
+
): string {
|
|
27
|
+
if (!dateStr) return '-'
|
|
28
|
+
const date = new Date(dateStr)
|
|
29
|
+
if (isNaN(date.getTime())) return '-'
|
|
30
|
+
|
|
31
|
+
if (Object.keys(options).length === 0) {
|
|
32
|
+
return date.toLocaleString()
|
|
33
|
+
}
|
|
34
|
+
return date.toLocaleString(undefined, options)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format to full datetime with year (20/12/2025 14:30)
|
|
39
|
+
*/
|
|
40
|
+
export function formatDateTime(dateStr: string | Date | null | undefined): string {
|
|
41
|
+
return formatDate(dateStr, {
|
|
42
|
+
year: 'numeric',
|
|
43
|
+
month: '2-digit',
|
|
44
|
+
day: '2-digit',
|
|
45
|
+
hour: '2-digit',
|
|
46
|
+
minute: '2-digit',
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format to short datetime without year (20/12 14:30)
|
|
52
|
+
*/
|
|
53
|
+
export function formatDateTimeShort(dateStr: string | Date | null | undefined): string {
|
|
54
|
+
return formatDate(dateStr, {
|
|
55
|
+
month: '2-digit',
|
|
56
|
+
day: '2-digit',
|
|
57
|
+
hour: '2-digit',
|
|
58
|
+
minute: '2-digit',
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format to date only (20/12/2025)
|
|
64
|
+
*/
|
|
65
|
+
export function formatDateOnly(dateStr: string | Date | null | undefined): string {
|
|
66
|
+
if (!dateStr) return '-'
|
|
67
|
+
const date = new Date(dateStr)
|
|
68
|
+
if (isNaN(date.getTime())) return '-'
|
|
69
|
+
return date.toLocaleDateString(undefined, {
|
|
70
|
+
year: 'numeric',
|
|
71
|
+
month: '2-digit',
|
|
72
|
+
day: '2-digit',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format to time only (14:30:00)
|
|
78
|
+
*/
|
|
79
|
+
export function formatTimeOnly(dateStr: string | Date | null | undefined): string {
|
|
80
|
+
if (!dateStr) return '-'
|
|
81
|
+
const date = new Date(dateStr)
|
|
82
|
+
if (isNaN(date.getTime())) return '-'
|
|
83
|
+
return date.toLocaleTimeString()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Format with "Never" as default (for last_used fields)
|
|
88
|
+
*/
|
|
89
|
+
export function formatDateOrNever(dateStr: string | Date | null | undefined): string {
|
|
90
|
+
if (!dateStr) return 'Never'
|
|
91
|
+
return formatDate(dateStr)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// DURATION FORMATTING
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format a duration in milliseconds to human-readable string
|
|
100
|
+
*
|
|
101
|
+
* @param ms - Duration in milliseconds
|
|
102
|
+
* @returns Human-readable duration (e.g., "2h 30m 15s" or "45.2s")
|
|
103
|
+
*/
|
|
104
|
+
export function formatDuration(ms: number | null | undefined): string {
|
|
105
|
+
if (ms === null || ms === undefined) return '-'
|
|
106
|
+
if (ms < 1000) return `${ms}ms`
|
|
107
|
+
|
|
108
|
+
const seconds = Math.floor(ms / 1000)
|
|
109
|
+
const minutes = Math.floor(seconds / 60)
|
|
110
|
+
const hours = Math.floor(minutes / 60)
|
|
111
|
+
|
|
112
|
+
if (hours > 0) {
|
|
113
|
+
const remainingMinutes = minutes % 60
|
|
114
|
+
const remainingSeconds = seconds % 60
|
|
115
|
+
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`
|
|
116
|
+
}
|
|
117
|
+
if (minutes > 0) {
|
|
118
|
+
const remainingSeconds = seconds % 60
|
|
119
|
+
return `${minutes}m ${remainingSeconds}s`
|
|
120
|
+
}
|
|
121
|
+
// Show decimal seconds for short durations
|
|
122
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format duration from start/end timestamps
|
|
127
|
+
*/
|
|
128
|
+
export function formatDurationBetween(
|
|
129
|
+
startDate: string | Date | null | undefined,
|
|
130
|
+
endDate: string | Date | null | undefined
|
|
131
|
+
): string {
|
|
132
|
+
if (!startDate || !endDate) return '-'
|
|
133
|
+
const start = new Date(startDate)
|
|
134
|
+
const end = new Date(endDate)
|
|
135
|
+
if (isNaN(start.getTime()) || isNaN(end.getTime())) return '-'
|
|
136
|
+
return formatDuration(end.getTime() - start.getTime())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// NUMBER FORMATTING
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Format a number with locale-specific separators
|
|
145
|
+
*/
|
|
146
|
+
export function formatNumber(
|
|
147
|
+
value: number | null | undefined,
|
|
148
|
+
options: Intl.NumberFormatOptions = {}
|
|
149
|
+
): string {
|
|
150
|
+
if (value === null || value === undefined) return '-'
|
|
151
|
+
return value.toLocaleString(undefined, options)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format bytes to human-readable size
|
|
156
|
+
*/
|
|
157
|
+
export function formatBytes(bytes: number | null | undefined): string {
|
|
158
|
+
if (bytes === null || bytes === undefined) return '-'
|
|
159
|
+
if (bytes === 0) return '0 B'
|
|
160
|
+
|
|
161
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
162
|
+
const k = 1024
|
|
163
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
164
|
+
|
|
165
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i]}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format a percentage
|
|
170
|
+
*/
|
|
171
|
+
export function formatPercent(value: number | null | undefined, decimals = 1): string {
|
|
172
|
+
if (value === null || value === undefined) return '-'
|
|
173
|
+
return `${(value * 100).toFixed(decimals)}%`
|
|
174
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm - Utils exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Formatters
|
|
6
|
+
export {
|
|
7
|
+
formatDate,
|
|
8
|
+
formatDateTime,
|
|
9
|
+
formatDateTimeShort,
|
|
10
|
+
formatDateOnly,
|
|
11
|
+
formatTimeOnly,
|
|
12
|
+
formatDateOrNever,
|
|
13
|
+
formatDuration,
|
|
14
|
+
formatDurationBetween,
|
|
15
|
+
formatNumber,
|
|
16
|
+
formatBytes,
|
|
17
|
+
formatPercent,
|
|
18
|
+
} from './formatters'
|
|
19
|
+
|
|
20
|
+
// Transformers
|
|
21
|
+
export {
|
|
22
|
+
toKeyValueArray,
|
|
23
|
+
toKeyValueObject,
|
|
24
|
+
toLanguageCodes,
|
|
25
|
+
toLanguageObjects,
|
|
26
|
+
toDateObject,
|
|
27
|
+
toIsoDate,
|
|
28
|
+
toIsoDateTime,
|
|
29
|
+
deepClone,
|
|
30
|
+
getSparseUpdate,
|
|
31
|
+
isEqual,
|
|
32
|
+
} from './transformers'
|
|
33
|
+
|
|
34
|
+
// Type exports
|
|
35
|
+
export type { KeyValuePair, LanguageObject } from './transformers'
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data transformation utilities for forms
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent conversions between API data formats and form structures.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Key-value pair structure for KeyValueEditor
|
|
9
|
+
*/
|
|
10
|
+
export interface KeyValuePair {
|
|
11
|
+
key: string
|
|
12
|
+
value: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Language object structure
|
|
17
|
+
*/
|
|
18
|
+
export interface LanguageObject {
|
|
19
|
+
code: string
|
|
20
|
+
fluency: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert object {key: value} to array [{key, value}] for KeyValueEditor
|
|
25
|
+
*/
|
|
26
|
+
export function toKeyValueArray(obj: Record<string, unknown> | null | undefined): KeyValuePair[] {
|
|
27
|
+
if (!obj || typeof obj !== 'object') return []
|
|
28
|
+
return Object.entries(obj).map(([key, value]) => ({ key, value }))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert array [{key, value}] to object {key: value} for API
|
|
33
|
+
*/
|
|
34
|
+
export function toKeyValueObject(arr: KeyValuePair[] | null | undefined): Record<string, unknown> {
|
|
35
|
+
if (!Array.isArray(arr)) return {}
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
arr.filter((item) => item.key && item.key.trim()).map((item) => [item.key.trim(), item.value])
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert language objects [{code, fluency}] to array of codes ['en', 'fr']
|
|
43
|
+
*/
|
|
44
|
+
export function toLanguageCodes(
|
|
45
|
+
languages: Array<string | LanguageObject> | null | undefined
|
|
46
|
+
): string[] {
|
|
47
|
+
if (!Array.isArray(languages)) return []
|
|
48
|
+
return languages
|
|
49
|
+
.map((l) => (typeof l === 'string' ? l : l.code))
|
|
50
|
+
.filter((code): code is string => Boolean(code))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convert array of codes to language objects [{code, fluency}]
|
|
55
|
+
*/
|
|
56
|
+
export function toLanguageObjects(
|
|
57
|
+
codes: Array<string | LanguageObject> | null | undefined,
|
|
58
|
+
defaultFluency = 1
|
|
59
|
+
): LanguageObject[] {
|
|
60
|
+
if (!Array.isArray(codes)) return []
|
|
61
|
+
return codes.filter(Boolean).map((code) => ({
|
|
62
|
+
code: typeof code === 'string' ? code : code.code,
|
|
63
|
+
fluency: typeof code === 'object' && code !== null ? code.fluency : defaultFluency,
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convert ISO date string to Date object
|
|
69
|
+
*/
|
|
70
|
+
export function toDateObject(dateStr: string | Date | null | undefined): Date | null {
|
|
71
|
+
if (!dateStr) return null
|
|
72
|
+
if (dateStr instanceof Date) return dateStr
|
|
73
|
+
return new Date(dateStr)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert Date to ISO date string (YYYY-MM-DD)
|
|
78
|
+
*/
|
|
79
|
+
export function toIsoDate(date: string | Date | null | undefined): string | null {
|
|
80
|
+
if (!date) return null
|
|
81
|
+
if (typeof date === 'string') return date.split('T')[0] ?? date
|
|
82
|
+
return date.toISOString().split('T')[0] ?? null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Convert Date to ISO datetime string
|
|
87
|
+
*/
|
|
88
|
+
export function toIsoDateTime(date: string | Date | null | undefined): string | null {
|
|
89
|
+
if (!date) return null
|
|
90
|
+
if (typeof date === 'string') return date
|
|
91
|
+
return date.toISOString()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deep clone an object (for snapshots)
|
|
96
|
+
*/
|
|
97
|
+
export function deepClone<T>(obj: T): T {
|
|
98
|
+
if (obj === null || typeof obj !== 'object') return obj
|
|
99
|
+
if (obj instanceof Date) return new Date(obj) as unknown as T
|
|
100
|
+
if (Array.isArray(obj)) return obj.map(deepClone) as unknown as T
|
|
101
|
+
return Object.fromEntries(
|
|
102
|
+
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [k, deepClone(v)])
|
|
103
|
+
) as unknown as T
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get sparse update object (only changed fields)
|
|
108
|
+
*/
|
|
109
|
+
export function getSparseUpdate<T extends Record<string, unknown>>(
|
|
110
|
+
original: T,
|
|
111
|
+
modified: T,
|
|
112
|
+
excludeFields: string[] = []
|
|
113
|
+
): Partial<T> {
|
|
114
|
+
const patch: Partial<T> = {}
|
|
115
|
+
for (const key in modified) {
|
|
116
|
+
if (excludeFields.includes(key)) continue
|
|
117
|
+
const origVal = JSON.stringify(original[key])
|
|
118
|
+
const modVal = JSON.stringify(modified[key])
|
|
119
|
+
if (origVal !== modVal) {
|
|
120
|
+
patch[key as keyof T] = modified[key]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return patch
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if two values are deeply equal
|
|
128
|
+
*/
|
|
129
|
+
export function isEqual(a: unknown, b: unknown): boolean {
|
|
130
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
131
|
+
}
|