@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,200 @@
|
|
|
1
|
+
import { IStorage } from './IStorage'
|
|
2
|
+
import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
|
|
3
|
+
import { StorageError } from './MemoryStorage'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LocalStorage options
|
|
7
|
+
*/
|
|
8
|
+
export interface LocalStorageOptions {
|
|
9
|
+
key: string
|
|
10
|
+
idField?: string
|
|
11
|
+
generateId?: () => string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* LocalStorage - Browser localStorage storage adapter
|
|
16
|
+
*
|
|
17
|
+
* Implements the storage interface using browser localStorage.
|
|
18
|
+
* Useful for offline-first applications, user preferences, draft saving.
|
|
19
|
+
*/
|
|
20
|
+
export class LocalStorage<T extends EntityRecord = EntityRecord> extends IStorage<T> {
|
|
21
|
+
static storageName = 'LocalStorage'
|
|
22
|
+
|
|
23
|
+
static capabilities: StorageCapabilities = {
|
|
24
|
+
supportsTotal: true,
|
|
25
|
+
supportsFilters: true,
|
|
26
|
+
supportsPagination: true,
|
|
27
|
+
supportsCaching: false,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @deprecated Use static LocalStorage.capabilities.supportsCaching instead */
|
|
31
|
+
get supportsCaching(): boolean {
|
|
32
|
+
return LocalStorage.capabilities.supportsCaching
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
readonly key: string
|
|
36
|
+
readonly idField: string
|
|
37
|
+
readonly generateId: () => string
|
|
38
|
+
|
|
39
|
+
constructor(options: LocalStorageOptions) {
|
|
40
|
+
super()
|
|
41
|
+
const {
|
|
42
|
+
key,
|
|
43
|
+
idField = 'id',
|
|
44
|
+
generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
|
|
45
|
+
} = options
|
|
46
|
+
|
|
47
|
+
this.key = key
|
|
48
|
+
this.idField = idField
|
|
49
|
+
this.generateId = generateId
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected _getAll(): T[] {
|
|
53
|
+
try {
|
|
54
|
+
const stored = localStorage.getItem(this.key)
|
|
55
|
+
return stored ? JSON.parse(stored) : []
|
|
56
|
+
} catch {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected _saveAll(items: T[]): void {
|
|
62
|
+
localStorage.setItem(this.key, JSON.stringify(items))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async list(params: ListParams = {}): Promise<ListResult<T>> {
|
|
66
|
+
const { page = 1, page_size = 20, sort_by, sort_order = 'asc', filters = {}, search } = params
|
|
67
|
+
|
|
68
|
+
let items = this._getAll()
|
|
69
|
+
|
|
70
|
+
// Apply filters (exact match for dropdown filters)
|
|
71
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
72
|
+
if (value === null || value === undefined || value === '') continue
|
|
73
|
+
items = items.filter((item) => {
|
|
74
|
+
const itemValue = item[key as keyof T]
|
|
75
|
+
if (typeof value === 'string' && typeof itemValue === 'string') {
|
|
76
|
+
return itemValue.toLowerCase() === value.toLowerCase()
|
|
77
|
+
}
|
|
78
|
+
return itemValue === value
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Apply search (substring match on all string fields)
|
|
83
|
+
if (search && typeof search === 'string' && search.trim()) {
|
|
84
|
+
const query = search.toLowerCase().trim()
|
|
85
|
+
items = items.filter((item) => {
|
|
86
|
+
for (const value of Object.values(item)) {
|
|
87
|
+
if (typeof value === 'string' && value.toLowerCase().includes(query)) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const total = items.length
|
|
96
|
+
|
|
97
|
+
// Apply sorting
|
|
98
|
+
if (sort_by) {
|
|
99
|
+
items.sort((a, b) => {
|
|
100
|
+
const aVal = a[sort_by as keyof T]
|
|
101
|
+
const bVal = b[sort_by as keyof T]
|
|
102
|
+
if (aVal === undefined || aVal === null) return 1
|
|
103
|
+
if (bVal === undefined || bVal === null) return -1
|
|
104
|
+
if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
|
|
105
|
+
if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
|
|
106
|
+
return 0
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Apply pagination
|
|
111
|
+
const start = (page - 1) * page_size
|
|
112
|
+
items = items.slice(start, start + page_size)
|
|
113
|
+
|
|
114
|
+
return { items, total }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async get(id: string | number): Promise<T> {
|
|
118
|
+
const items = this._getAll()
|
|
119
|
+
const item = items.find((i) => i[this.idField as keyof T] === id)
|
|
120
|
+
if (!item) {
|
|
121
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
122
|
+
}
|
|
123
|
+
return item
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getMany(ids: Array<string | number>): Promise<T[]> {
|
|
127
|
+
if (!ids || ids.length === 0) return []
|
|
128
|
+
const items = this._getAll()
|
|
129
|
+
const idSet = new Set(ids)
|
|
130
|
+
return items.filter((i) => idSet.has(i[this.idField as keyof T] as string | number))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async create(data: Partial<T>): Promise<T> {
|
|
134
|
+
const items = this._getAll()
|
|
135
|
+
const newItem = {
|
|
136
|
+
...data,
|
|
137
|
+
[this.idField]: (data[this.idField as keyof T] as string | number) || this.generateId(),
|
|
138
|
+
created_at: (data as Record<string, unknown>).created_at || new Date().toISOString(),
|
|
139
|
+
} as unknown as T
|
|
140
|
+
items.push(newItem)
|
|
141
|
+
this._saveAll(items)
|
|
142
|
+
return newItem
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async update(id: string | number, data: Partial<T>): Promise<T> {
|
|
146
|
+
const items = this._getAll()
|
|
147
|
+
const index = items.findIndex((i) => i[this.idField as keyof T] === id)
|
|
148
|
+
if (index === -1) {
|
|
149
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
150
|
+
}
|
|
151
|
+
const updated = {
|
|
152
|
+
...data,
|
|
153
|
+
[this.idField]: id,
|
|
154
|
+
updated_at: new Date().toISOString(),
|
|
155
|
+
} as unknown as T
|
|
156
|
+
items[index] = updated
|
|
157
|
+
this._saveAll(items)
|
|
158
|
+
return updated
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async patch(id: string | number, data: Partial<T>): Promise<T> {
|
|
162
|
+
const items = this._getAll()
|
|
163
|
+
const index = items.findIndex((i) => i[this.idField as keyof T] === id)
|
|
164
|
+
if (index === -1) {
|
|
165
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
166
|
+
}
|
|
167
|
+
const updated = {
|
|
168
|
+
...items[index],
|
|
169
|
+
...data,
|
|
170
|
+
updated_at: new Date().toISOString(),
|
|
171
|
+
} as unknown as T
|
|
172
|
+
items[index] = updated
|
|
173
|
+
this._saveAll(items)
|
|
174
|
+
return updated
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async delete(id: string | number): Promise<void> {
|
|
178
|
+
const items = this._getAll()
|
|
179
|
+
const index = items.findIndex((i) => i[this.idField as keyof T] === id)
|
|
180
|
+
if (index === -1) {
|
|
181
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
182
|
+
}
|
|
183
|
+
items.splice(index, 1)
|
|
184
|
+
this._saveAll(items)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async clear(): Promise<void> {
|
|
188
|
+
localStorage.removeItem(this.key)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
reset(): void {
|
|
192
|
+
localStorage.removeItem(this.key)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function createLocalStorage<T extends EntityRecord = EntityRecord>(
|
|
197
|
+
options: LocalStorageOptions
|
|
198
|
+
): LocalStorage<T> {
|
|
199
|
+
return new LocalStorage(options)
|
|
200
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { IStorage } from './IStorage'
|
|
2
|
+
import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MemoryStorage options
|
|
6
|
+
*/
|
|
7
|
+
export interface MemoryStorageOptions<T extends EntityRecord = EntityRecord> {
|
|
8
|
+
idField?: string
|
|
9
|
+
generateId?: () => string
|
|
10
|
+
initialData?: T[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Storage error with HTTP-like status
|
|
15
|
+
*/
|
|
16
|
+
export class StorageError extends Error {
|
|
17
|
+
status: number
|
|
18
|
+
constructor(message: string, status: number) {
|
|
19
|
+
super(message)
|
|
20
|
+
this.status = status
|
|
21
|
+
this.name = 'StorageError'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* MemoryStorage - In-memory storage adapter
|
|
27
|
+
*
|
|
28
|
+
* Implements the storage interface using in-memory Map.
|
|
29
|
+
* Useful for testing, ephemeral data, caching layer.
|
|
30
|
+
*/
|
|
31
|
+
export class MemoryStorage<T extends EntityRecord = EntityRecord> extends IStorage<T> {
|
|
32
|
+
static storageName = 'MemoryStorage'
|
|
33
|
+
|
|
34
|
+
static capabilities: StorageCapabilities = {
|
|
35
|
+
supportsTotal: true,
|
|
36
|
+
supportsFilters: true,
|
|
37
|
+
supportsPagination: true,
|
|
38
|
+
supportsCaching: false,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @deprecated Use static MemoryStorage.capabilities.supportsCaching instead */
|
|
42
|
+
get supportsCaching(): boolean {
|
|
43
|
+
return MemoryStorage.capabilities.supportsCaching
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
readonly idField: string
|
|
47
|
+
readonly generateId: () => string
|
|
48
|
+
protected _data: Map<string, T>
|
|
49
|
+
|
|
50
|
+
constructor(options: MemoryStorageOptions<T> = {}) {
|
|
51
|
+
super()
|
|
52
|
+
const {
|
|
53
|
+
idField = 'id',
|
|
54
|
+
generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
|
|
55
|
+
initialData = [],
|
|
56
|
+
} = options
|
|
57
|
+
|
|
58
|
+
this.idField = idField
|
|
59
|
+
this.generateId = generateId
|
|
60
|
+
this._data = new Map()
|
|
61
|
+
|
|
62
|
+
// Initialize with initial data
|
|
63
|
+
for (const item of initialData) {
|
|
64
|
+
const id = item[idField as keyof T]
|
|
65
|
+
if (id) {
|
|
66
|
+
this._data.set(String(id), { ...item })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected _getAll(): T[] {
|
|
72
|
+
return Array.from(this._data.values())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async list(params: ListParams = {}): Promise<ListResult<T>> {
|
|
76
|
+
const { page = 1, page_size = 20, sort_by, sort_order = 'asc', filters = {} } = params
|
|
77
|
+
|
|
78
|
+
let items = this._getAll()
|
|
79
|
+
|
|
80
|
+
// Apply filters
|
|
81
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
82
|
+
if (value === null || value === undefined || value === '') continue
|
|
83
|
+
items = items.filter((item) => {
|
|
84
|
+
const itemValue = item[key as keyof T]
|
|
85
|
+
if (typeof value === 'string' && typeof itemValue === 'string') {
|
|
86
|
+
return itemValue.toLowerCase().includes(value.toLowerCase())
|
|
87
|
+
}
|
|
88
|
+
return itemValue === value
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const total = items.length
|
|
93
|
+
|
|
94
|
+
// Apply sorting
|
|
95
|
+
if (sort_by) {
|
|
96
|
+
items.sort((a, b) => {
|
|
97
|
+
const aVal = a[sort_by as keyof T]
|
|
98
|
+
const bVal = b[sort_by as keyof T]
|
|
99
|
+
if (aVal === undefined || aVal === null) return 1
|
|
100
|
+
if (bVal === undefined || bVal === null) return -1
|
|
101
|
+
if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
|
|
102
|
+
if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
|
|
103
|
+
return 0
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Apply pagination
|
|
108
|
+
const start = (page - 1) * page_size
|
|
109
|
+
items = items.slice(start, start + page_size)
|
|
110
|
+
|
|
111
|
+
return { items, total }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async get(id: string | number): Promise<T> {
|
|
115
|
+
const item = this._data.get(String(id))
|
|
116
|
+
if (!item) {
|
|
117
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
118
|
+
}
|
|
119
|
+
return { ...item }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async create(data: Partial<T>): Promise<T> {
|
|
123
|
+
const id = (data[this.idField as keyof T] as string | number) || this.generateId()
|
|
124
|
+
const newItem = {
|
|
125
|
+
...data,
|
|
126
|
+
[this.idField]: id,
|
|
127
|
+
created_at: (data as Record<string, unknown>).created_at || new Date().toISOString(),
|
|
128
|
+
} as unknown as T
|
|
129
|
+
this._data.set(String(id), newItem)
|
|
130
|
+
return { ...newItem }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async update(id: string | number, data: Partial<T>): Promise<T> {
|
|
134
|
+
if (!this._data.has(String(id))) {
|
|
135
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
136
|
+
}
|
|
137
|
+
const updated = {
|
|
138
|
+
...data,
|
|
139
|
+
[this.idField]: id,
|
|
140
|
+
updated_at: new Date().toISOString(),
|
|
141
|
+
} as unknown as T
|
|
142
|
+
this._data.set(String(id), updated)
|
|
143
|
+
return { ...updated }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async patch(id: string | number, data: Partial<T>): Promise<T> {
|
|
147
|
+
const existing = this._data.get(String(id))
|
|
148
|
+
if (!existing) {
|
|
149
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
150
|
+
}
|
|
151
|
+
const updated = {
|
|
152
|
+
...existing,
|
|
153
|
+
...data,
|
|
154
|
+
updated_at: new Date().toISOString(),
|
|
155
|
+
} as T
|
|
156
|
+
this._data.set(String(id), updated)
|
|
157
|
+
return { ...updated }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async delete(id: string | number): Promise<void> {
|
|
161
|
+
if (!this._data.has(String(id))) {
|
|
162
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
163
|
+
}
|
|
164
|
+
this._data.delete(String(id))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async clear(): Promise<void> {
|
|
168
|
+
this._data.clear()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get size(): number {
|
|
172
|
+
return this._data.size
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
reset(): void {
|
|
176
|
+
this._data.clear()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createMemoryStorage<T extends EntityRecord = EntityRecord>(
|
|
181
|
+
options?: MemoryStorageOptions<T>
|
|
182
|
+
): MemoryStorage<T> {
|
|
183
|
+
return new MemoryStorage(options)
|
|
184
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { IStorage } from './IStorage'
|
|
2
|
+
import { StorageError } from './MemoryStorage'
|
|
3
|
+
import { QueryExecutor } from '../../query'
|
|
4
|
+
import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MockApiStorage options
|
|
8
|
+
*/
|
|
9
|
+
export interface MockApiStorageOptions<T extends EntityRecord = EntityRecord> {
|
|
10
|
+
entityName: string
|
|
11
|
+
idField?: string
|
|
12
|
+
generateId?: () => string
|
|
13
|
+
initialData?: T[] | null
|
|
14
|
+
authCheck?: (() => boolean) | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MockApiStorage - In-memory storage with localStorage persistence
|
|
19
|
+
*
|
|
20
|
+
* Combines in-memory Map performance with localStorage persistence.
|
|
21
|
+
* Designed for demos that need:
|
|
22
|
+
* - Fast in-memory operations
|
|
23
|
+
* - Data persistence across page reloads
|
|
24
|
+
* - Optional initial data seeding
|
|
25
|
+
*
|
|
26
|
+
* localStorage key pattern: mockapi_${entityName}_data
|
|
27
|
+
*/
|
|
28
|
+
export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStorage<T> {
|
|
29
|
+
static storageName = 'MockApiStorage'
|
|
30
|
+
|
|
31
|
+
static capabilities: StorageCapabilities = {
|
|
32
|
+
supportsTotal: true,
|
|
33
|
+
supportsFilters: true,
|
|
34
|
+
supportsPagination: true,
|
|
35
|
+
supportsCaching: false,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @deprecated Use static MockApiStorage.capabilities.supportsCaching instead */
|
|
39
|
+
get supportsCaching(): boolean {
|
|
40
|
+
return MockApiStorage.capabilities.supportsCaching
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Instance capabilities getter.
|
|
45
|
+
* Merges static capabilities with instance-specific ones like requiresAuth.
|
|
46
|
+
*/
|
|
47
|
+
get capabilities(): StorageCapabilities & { requiresAuth: boolean } {
|
|
48
|
+
return {
|
|
49
|
+
...MockApiStorage.capabilities,
|
|
50
|
+
requiresAuth: !!this._authCheck,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
readonly entityName: string
|
|
55
|
+
readonly idField: string
|
|
56
|
+
readonly generateId: () => string
|
|
57
|
+
|
|
58
|
+
protected _storageKey: string
|
|
59
|
+
protected _data: Map<string, T>
|
|
60
|
+
protected _authCheck: (() => boolean) | null
|
|
61
|
+
|
|
62
|
+
constructor(options: MockApiStorageOptions<T>) {
|
|
63
|
+
super()
|
|
64
|
+
const {
|
|
65
|
+
entityName,
|
|
66
|
+
idField = 'id',
|
|
67
|
+
generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
|
|
68
|
+
initialData = null,
|
|
69
|
+
authCheck = null,
|
|
70
|
+
} = options
|
|
71
|
+
|
|
72
|
+
if (!entityName) {
|
|
73
|
+
throw new Error('MockApiStorage requires entityName option')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.entityName = entityName
|
|
77
|
+
this.idField = idField
|
|
78
|
+
this.generateId = generateId
|
|
79
|
+
this._storageKey = `mockapi_${entityName}_data`
|
|
80
|
+
this._data = new Map()
|
|
81
|
+
this._authCheck = authCheck
|
|
82
|
+
|
|
83
|
+
this._loadFromStorage(initialData)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected _checkAuth(): void {
|
|
87
|
+
if (this._authCheck && !this._authCheck()) {
|
|
88
|
+
throw new StorageError(
|
|
89
|
+
`Unauthorized: Authentication required to access ${this.entityName}`,
|
|
90
|
+
401
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected _loadFromStorage(initialData: T[] | null): void {
|
|
96
|
+
try {
|
|
97
|
+
const stored = localStorage.getItem(this._storageKey)
|
|
98
|
+
if (stored) {
|
|
99
|
+
const items = JSON.parse(stored) as T[]
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
const id = item[this.idField as keyof T]
|
|
102
|
+
if (id !== undefined && id !== null) {
|
|
103
|
+
this._data.set(String(id), { ...item })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (initialData && Array.isArray(initialData) && initialData.length > 0) {
|
|
107
|
+
for (const item of initialData) {
|
|
108
|
+
const id = item[this.idField as keyof T]
|
|
109
|
+
if (id !== undefined && id !== null) {
|
|
110
|
+
this._data.set(String(id), { ...item })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this._persistToStorage()
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
if (initialData && Array.isArray(initialData)) {
|
|
117
|
+
for (const item of initialData) {
|
|
118
|
+
const id = item[this.idField as keyof T]
|
|
119
|
+
if (id !== undefined && id !== null) {
|
|
120
|
+
this._data.set(String(id), { ...item })
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected _persistToStorage(): void {
|
|
128
|
+
try {
|
|
129
|
+
const items = Array.from(this._data.values())
|
|
130
|
+
localStorage.setItem(this._storageKey, JSON.stringify(items))
|
|
131
|
+
} catch {
|
|
132
|
+
// localStorage unavailable or quota exceeded, continue without persistence
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected _getAll(): T[] {
|
|
137
|
+
return Array.from(this._data.values())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async list(params: ListParams = {}): Promise<ListResult<T>> {
|
|
141
|
+
this._checkAuth()
|
|
142
|
+
const { page = 1, page_size = 20, sort_by, sort_order = 'asc', filters = {}, search } = params
|
|
143
|
+
|
|
144
|
+
let items = this._getAll()
|
|
145
|
+
|
|
146
|
+
// Apply filters using QueryExecutor (supports MongoDB-like operators)
|
|
147
|
+
if (Object.keys(filters).length > 0) {
|
|
148
|
+
items = QueryExecutor.execute(items, filters as Parameters<typeof QueryExecutor.execute>[1])
|
|
149
|
+
.items as T[]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Apply search (substring match on all string fields)
|
|
153
|
+
if (search && typeof search === 'string' && search.trim()) {
|
|
154
|
+
const query = search.toLowerCase().trim()
|
|
155
|
+
items = items.filter((item) => {
|
|
156
|
+
for (const value of Object.values(item)) {
|
|
157
|
+
if (typeof value === 'string' && value.toLowerCase().includes(query)) {
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const total = items.length
|
|
166
|
+
|
|
167
|
+
// Apply sorting
|
|
168
|
+
if (sort_by) {
|
|
169
|
+
items.sort((a, b) => {
|
|
170
|
+
const aVal = a[sort_by as keyof T]
|
|
171
|
+
const bVal = b[sort_by as keyof T]
|
|
172
|
+
if (aVal === undefined || aVal === null) return 1
|
|
173
|
+
if (bVal === undefined || bVal === null) return -1
|
|
174
|
+
if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
|
|
175
|
+
if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
|
|
176
|
+
return 0
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Apply pagination
|
|
181
|
+
const start = (page - 1) * page_size
|
|
182
|
+
items = items.slice(start, start + page_size)
|
|
183
|
+
|
|
184
|
+
return { items, total }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async get(id: string | number): Promise<T> {
|
|
188
|
+
this._checkAuth()
|
|
189
|
+
const item = this._data.get(String(id))
|
|
190
|
+
if (!item) {
|
|
191
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
192
|
+
}
|
|
193
|
+
return { ...item }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async getMany(ids: Array<string | number>): Promise<T[]> {
|
|
197
|
+
this._checkAuth()
|
|
198
|
+
if (!ids || ids.length === 0) return []
|
|
199
|
+
const results: T[] = []
|
|
200
|
+
for (const id of ids) {
|
|
201
|
+
const item = this._data.get(String(id))
|
|
202
|
+
if (item) {
|
|
203
|
+
results.push({ ...item })
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return results
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async distinct(field: string): Promise<unknown[]> {
|
|
210
|
+
const values = new Set<unknown>()
|
|
211
|
+
for (const item of this._data.values()) {
|
|
212
|
+
const value = item[field as keyof T]
|
|
213
|
+
if (value !== undefined && value !== null) {
|
|
214
|
+
values.add(value)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return Array.from(values).sort()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async distinctWithCount(field: string): Promise<Array<{ value: unknown; count: number }>> {
|
|
221
|
+
const counts = new Map<unknown, number>()
|
|
222
|
+
for (const item of this._data.values()) {
|
|
223
|
+
const value = item[field as keyof T]
|
|
224
|
+
if (value !== undefined && value !== null) {
|
|
225
|
+
counts.set(value, (counts.get(value) || 0) + 1)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return Array.from(counts.entries())
|
|
229
|
+
.map(([value, count]) => ({ value, count }))
|
|
230
|
+
.sort((a, b) => {
|
|
231
|
+
if (a.value === null || a.value === undefined) return 1
|
|
232
|
+
if (b.value === null || b.value === undefined) return -1
|
|
233
|
+
if (a.value < b.value) return -1
|
|
234
|
+
if (a.value > b.value) return 1
|
|
235
|
+
return 0
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async create(data: Partial<T>): Promise<T> {
|
|
240
|
+
this._checkAuth()
|
|
241
|
+
const id =
|
|
242
|
+
(data[this.idField as keyof T] as string | number | undefined) || this.generateId()
|
|
243
|
+
const newItem = {
|
|
244
|
+
...data,
|
|
245
|
+
[this.idField]: id,
|
|
246
|
+
created_at: (data as Record<string, unknown>).created_at || new Date().toISOString(),
|
|
247
|
+
} as unknown as T
|
|
248
|
+
this._data.set(String(id), newItem)
|
|
249
|
+
this._persistToStorage()
|
|
250
|
+
return { ...newItem }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async update(id: string | number, data: Partial<T>): Promise<T> {
|
|
254
|
+
this._checkAuth()
|
|
255
|
+
if (!this._data.has(String(id))) {
|
|
256
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
257
|
+
}
|
|
258
|
+
const updated = {
|
|
259
|
+
...data,
|
|
260
|
+
[this.idField]: id,
|
|
261
|
+
updated_at: new Date().toISOString(),
|
|
262
|
+
} as unknown as T
|
|
263
|
+
this._data.set(String(id), updated)
|
|
264
|
+
this._persistToStorage()
|
|
265
|
+
return { ...updated }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async patch(id: string | number, data: Partial<T>): Promise<T> {
|
|
269
|
+
this._checkAuth()
|
|
270
|
+
const existing = this._data.get(String(id))
|
|
271
|
+
if (!existing) {
|
|
272
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
273
|
+
}
|
|
274
|
+
const updated = {
|
|
275
|
+
...existing,
|
|
276
|
+
...data,
|
|
277
|
+
updated_at: new Date().toISOString(),
|
|
278
|
+
} as T
|
|
279
|
+
this._data.set(String(id), updated)
|
|
280
|
+
this._persistToStorage()
|
|
281
|
+
return { ...updated }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async delete(id: string | number): Promise<void> {
|
|
285
|
+
this._checkAuth()
|
|
286
|
+
if (!this._data.has(String(id))) {
|
|
287
|
+
throw new StorageError(`Entity not found: ${id}`, 404)
|
|
288
|
+
}
|
|
289
|
+
this._data.delete(String(id))
|
|
290
|
+
this._persistToStorage()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async clear(): Promise<void> {
|
|
294
|
+
this._data.clear()
|
|
295
|
+
try {
|
|
296
|
+
localStorage.removeItem(this._storageKey)
|
|
297
|
+
} catch {
|
|
298
|
+
// localStorage unavailable
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
get size(): number {
|
|
303
|
+
return this._data.size
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
get storageKey(): string {
|
|
307
|
+
return this._storageKey
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Factory function to create a MockApiStorage
|
|
313
|
+
*/
|
|
314
|
+
export function createMockApiStorage<T extends EntityRecord = EntityRecord>(
|
|
315
|
+
options: MockApiStorageOptions<T>
|
|
316
|
+
): MockApiStorage<T> {
|
|
317
|
+
return new MockApiStorage(options)
|
|
318
|
+
}
|