@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.
Files changed (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. 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
+ }