@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,189 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import {
3
+ managerFactory,
4
+ defaultManagerResolver,
5
+ createManagerFactory,
6
+ createManagers
7
+ } from './factory'
8
+ import { EntityManager } from './EntityManager'
9
+ import { ApiStorage } from './storage/ApiStorage'
10
+ import { MemoryStorage } from './storage/MemoryStorage'
11
+
12
+ describe('defaultManagerResolver', () => {
13
+ it('creates EntityManager with config', () => {
14
+ const storage = new MemoryStorage()
15
+ const config = { storage, label: 'Bot' }
16
+
17
+ const manager = defaultManagerResolver(config, 'bots')
18
+
19
+ expect(manager).toBeInstanceOf(EntityManager)
20
+ expect(manager.name).toBe('bots')
21
+ expect(manager.storage).toBe(storage)
22
+ })
23
+
24
+ it('uses registered manager class from registry', () => {
25
+ class CustomManager extends EntityManager {}
26
+ const storage = new MemoryStorage()
27
+ const config = { storage }
28
+ const context = { managerRegistry: { bots: CustomManager } }
29
+
30
+ const manager = defaultManagerResolver(config, 'bots', context)
31
+
32
+ expect(manager).toBeInstanceOf(CustomManager)
33
+ })
34
+
35
+ it('falls back to EntityManager when not in registry', () => {
36
+ const storage = new MemoryStorage()
37
+ const config = { storage }
38
+ const context = { managerRegistry: { other: class extends EntityManager {} } }
39
+
40
+ const manager = defaultManagerResolver(config, 'bots', context)
41
+
42
+ expect(manager).toBeInstanceOf(EntityManager)
43
+ expect(manager.constructor).toBe(EntityManager)
44
+ })
45
+ })
46
+
47
+ describe('managerFactory', () => {
48
+ it('returns EntityManager instance directly', () => {
49
+ const manager = new EntityManager({ name: 'test', storage: new MemoryStorage() })
50
+ const result = managerFactory(manager, 'test')
51
+ expect(result).toBe(manager)
52
+ })
53
+
54
+ it('returns duck-typed manager directly', () => {
55
+ const duckManager = {
56
+ storage: new MemoryStorage(),
57
+ list: () => Promise.resolve({ items: [], total: 0 }),
58
+ get: () => Promise.resolve(null)
59
+ }
60
+ const result = managerFactory(duckManager, 'test')
61
+ expect(result).toBe(duckManager)
62
+ })
63
+
64
+ it('creates manager from string pattern', () => {
65
+ const result = managerFactory('api:/api/bots', 'bots')
66
+ expect(result).toBeInstanceOf(EntityManager)
67
+ expect(result.storage).toBeInstanceOf(ApiStorage)
68
+ expect(result.storage.endpoint).toBe('/api/bots')
69
+ })
70
+
71
+ it('creates manager from config object with storage string', () => {
72
+ const result = managerFactory({
73
+ storage: 'api:/api/tasks',
74
+ label: 'Task'
75
+ }, 'tasks')
76
+
77
+ expect(result).toBeInstanceOf(EntityManager)
78
+ expect(result.storage).toBeInstanceOf(ApiStorage)
79
+ })
80
+
81
+ it('uses storage instance directly from config', () => {
82
+ const storage = new MemoryStorage()
83
+ const result = managerFactory({ storage }, 'items')
84
+
85
+ expect(result).toBeInstanceOf(EntityManager)
86
+ expect(result.storage).toBe(storage)
87
+ })
88
+
89
+ it('uses custom storageResolver', () => {
90
+ const customStorage = new MemoryStorage()
91
+ const storageResolver = vi.fn().mockReturnValue(customStorage)
92
+ const context = { storageResolver }
93
+
94
+ const result = managerFactory('api:/test', 'test', context)
95
+
96
+ expect(storageResolver).toHaveBeenCalled()
97
+ expect(result.storage).toBe(customStorage)
98
+ })
99
+
100
+ it('uses custom managerResolver', () => {
101
+ class CustomManager extends EntityManager {}
102
+ const managerResolver = vi.fn().mockImplementation((config, name) => {
103
+ return new CustomManager({ name, ...config })
104
+ })
105
+ const context = { managerResolver }
106
+
107
+ const result = managerFactory('api:/test', 'test', context)
108
+
109
+ expect(managerResolver).toHaveBeenCalled()
110
+ expect(result).toBeInstanceOf(CustomManager)
111
+ })
112
+
113
+ it('uses managerRegistry for generated classes', () => {
114
+ class BotManager extends EntityManager {}
115
+ const context = { managerRegistry: { bots: BotManager } }
116
+
117
+ const result = managerFactory('api:/api/bots', 'bots', context)
118
+
119
+ expect(result).toBeInstanceOf(BotManager)
120
+ })
121
+
122
+ it('throws for invalid config', () => {
123
+ expect(() => managerFactory(123, 'test'))
124
+ .toThrow('Invalid manager config')
125
+ })
126
+ })
127
+
128
+ describe('createManagerFactory', () => {
129
+ it('creates factory with bound context', () => {
130
+ class CustomManager extends EntityManager {}
131
+ const context = { managerRegistry: { bots: CustomManager } }
132
+ const factory = createManagerFactory(context)
133
+
134
+ const result = factory('api:/api/bots', 'bots')
135
+
136
+ expect(result).toBeInstanceOf(CustomManager)
137
+ })
138
+ })
139
+
140
+ describe('createManagers', () => {
141
+ it('creates managers from config object', () => {
142
+ const config = {
143
+ bots: 'api:/api/bots',
144
+ tasks: { storage: 'memory:tasks', label: 'Task' }
145
+ }
146
+
147
+ const managers = createManagers(config)
148
+
149
+ expect(managers.bots).toBeInstanceOf(EntityManager)
150
+ expect(managers.bots.storage).toBeInstanceOf(ApiStorage)
151
+ expect(managers.tasks).toBeInstanceOf(EntityManager)
152
+ expect(managers.tasks.storage).toBeInstanceOf(MemoryStorage)
153
+ })
154
+
155
+ it('uses context for all managers', () => {
156
+ class BotManager extends EntityManager {}
157
+ class TaskManager extends EntityManager {}
158
+ const context = {
159
+ managerRegistry: { bots: BotManager, tasks: TaskManager }
160
+ }
161
+ const config = {
162
+ bots: 'api:/api/bots',
163
+ tasks: 'api:/api/tasks'
164
+ }
165
+
166
+ const managers = createManagers(config, context)
167
+
168
+ expect(managers.bots).toBeInstanceOf(BotManager)
169
+ expect(managers.tasks).toBeInstanceOf(TaskManager)
170
+ })
171
+
172
+ it('passes through existing manager instances', () => {
173
+ const existingManager = new EntityManager({ name: 'existing', storage: new MemoryStorage() })
174
+ const config = {
175
+ bots: 'api:/api/bots',
176
+ existing: existingManager
177
+ }
178
+
179
+ const managers = createManagers(config)
180
+
181
+ expect(managers.existing).toBe(existingManager)
182
+ expect(managers.bots).toBeInstanceOf(EntityManager)
183
+ })
184
+
185
+ it('returns empty object for empty config', () => {
186
+ const managers = createManagers({})
187
+ expect(managers).toEqual({})
188
+ })
189
+ })
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Manager Factory/Resolver Pattern
3
+ *
4
+ * Enables declarative manager configuration with auto-resolution.
5
+ * Works with storageFactory for complete auto-wiring.
6
+ */
7
+
8
+ /* eslint-disable @typescript-eslint/no-explicit-any */
9
+
10
+ import { EntityManager } from './EntityManager'
11
+ import { storageFactory } from './storage/factory'
12
+
13
+ /**
14
+ * Manager config interface
15
+ */
16
+ export interface ManagerConfig {
17
+ name?: string
18
+ storage?: any
19
+ label?: string
20
+ labelPlural?: string
21
+ fields?: Record<string, unknown>
22
+ [key: string]: unknown
23
+ }
24
+
25
+ /**
26
+ * Manager factory context
27
+ */
28
+ export interface ManagerFactoryContext {
29
+ managerRegistry?: Record<string, typeof EntityManager>
30
+ storageResolver?: (config: any, entityName: string) => any
31
+ managerResolver?: ManagerResolver
32
+ [key: string]: unknown
33
+ }
34
+
35
+ /**
36
+ * Manager resolver function type
37
+ */
38
+ export type ManagerResolver = (
39
+ config: ManagerConfig,
40
+ entityName: string,
41
+ context: ManagerFactoryContext
42
+ ) => EntityManager
43
+
44
+ /**
45
+ * Default manager resolver - creates manager instance from config
46
+ */
47
+ export function defaultManagerResolver(
48
+ config: ManagerConfig,
49
+ entityName: string,
50
+ context: ManagerFactoryContext = {}
51
+ ): EntityManager {
52
+ const { managerRegistry = {} } = context
53
+
54
+ // Look up registered manager class
55
+ const ManagerClass = managerRegistry[entityName] || EntityManager
56
+
57
+ // Ensure name is set
58
+ const managerConfig = {
59
+ name: entityName,
60
+ ...config
61
+ }
62
+
63
+ return new ManagerClass(managerConfig as any)
64
+ }
65
+
66
+ /**
67
+ * Manager factory - normalizes input and delegates to resolver
68
+ */
69
+ export function managerFactory(
70
+ config: EntityManager | string | ManagerConfig,
71
+ entityName: string,
72
+ context: ManagerFactoryContext = {}
73
+ ): EntityManager {
74
+ const {
75
+ storageResolver,
76
+ managerResolver = defaultManagerResolver
77
+ } = context
78
+
79
+ // Already a Manager instance → return directly
80
+ if (config instanceof EntityManager) {
81
+ return config
82
+ }
83
+
84
+ // Also check for duck-typed manager
85
+ if (
86
+ config &&
87
+ typeof config === 'object' &&
88
+ 'storage' in config &&
89
+ typeof (config as any).list === 'function'
90
+ ) {
91
+ return config as any
92
+ }
93
+
94
+ // String pattern → treat as storage config
95
+ if (typeof config === 'string') {
96
+ const storage = storageFactory(config, entityName, storageResolver as any)
97
+ return managerResolver({ storage }, entityName, context)
98
+ }
99
+
100
+ // Config object → resolve storage first, then manager
101
+ if (config && typeof config === 'object') {
102
+ const resolvedConfig = { ...config }
103
+
104
+ // Resolve storage if provided as string/config
105
+ if (config.storage && typeof config.storage === 'string') {
106
+ resolvedConfig.storage = storageFactory(config.storage, entityName, storageResolver as any)
107
+ } else if (
108
+ config.storage &&
109
+ typeof config.storage === 'object' &&
110
+ typeof (config.storage as any).list !== 'function'
111
+ ) {
112
+ resolvedConfig.storage = storageFactory(config.storage as any, entityName, storageResolver as any)
113
+ }
114
+
115
+ return managerResolver(resolvedConfig, entityName, context)
116
+ }
117
+
118
+ throw new Error(
119
+ `Invalid manager config for "${entityName}": ${typeof config}. Expected string, object, or Manager instance.`
120
+ )
121
+ }
122
+
123
+ /**
124
+ * Create a custom manager factory with context
125
+ */
126
+ export function createManagerFactory(
127
+ context: ManagerFactoryContext
128
+ ): (config: EntityManager | string | ManagerConfig, entityName: string) => EntityManager {
129
+ return (config, entityName) => managerFactory(config, entityName, context)
130
+ }
131
+
132
+ /**
133
+ * Create all managers from a config object
134
+ */
135
+ export function createManagers(
136
+ managersConfig: Record<string, EntityManager | string | ManagerConfig>,
137
+ context: ManagerFactoryContext = {}
138
+ ): Record<string, EntityManager> {
139
+ const managers: Record<string, EntityManager> = {}
140
+
141
+ for (const [entityName, config] of Object.entries(managersConfig)) {
142
+ managers[entityName] = managerFactory(config, entityName, context)
143
+ }
144
+
145
+ return managers
146
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Entity Module
3
+ *
4
+ * EntityManager and storage adapters for data abstraction.
5
+ */
6
+
7
+ // EntityManager
8
+ export {
9
+ EntityManager,
10
+ createEntityManager,
11
+ type EntityManagerOptions,
12
+ type RoutingContext,
13
+ type PresaveContext,
14
+ type PostsaveContext,
15
+ type PredeleteContext,
16
+ type QueryOptions,
17
+ type WhitelistContext,
18
+ type CacheInfo
19
+ } from './EntityManager'
20
+
21
+ // Re-export types from types module
22
+ export type {
23
+ EntityRecord,
24
+ ListParams,
25
+ ListResult,
26
+ FieldConfig,
27
+ ChildConfig,
28
+ ParentConfig,
29
+ RelationConfig,
30
+ NavConfig,
31
+ StorageCapabilities
32
+ } from '../types'
33
+
34
+ // Manager Factory
35
+ export {
36
+ managerFactory,
37
+ defaultManagerResolver,
38
+ createManagerFactory,
39
+ createManagers,
40
+ type ManagerConfig,
41
+ type ManagerFactoryContext,
42
+ type ManagerResolver
43
+ } from './factory'
44
+
45
+ // Storage adapters
46
+ export * from './storage/index'
47
+
48
+ // Auth adapters
49
+ export * from './auth'
@@ -0,0 +1,228 @@
1
+ import { IStorage } from './IStorage'
2
+ import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
3
+
4
+ /**
5
+ * HTTP client interface compatible with axios
6
+ */
7
+ export interface HttpClient {
8
+ get<T = unknown>(url: string, config?: { params?: Record<string, unknown> }): Promise<{ data: T }>
9
+ post<T = unknown>(url: string, data?: unknown): Promise<{ data: T }>
10
+ put<T = unknown>(url: string, data?: unknown): Promise<{ data: T }>
11
+ patch<T = unknown>(url: string, data?: unknown): Promise<{ data: T }>
12
+ delete<T = unknown>(url: string): Promise<{ data: T }>
13
+ request<T = unknown>(config: {
14
+ method: string
15
+ url: string
16
+ data?: unknown
17
+ params?: Record<string, unknown>
18
+ headers?: Record<string, string>
19
+ }): Promise<{ data: T }>
20
+ }
21
+
22
+ /**
23
+ * Routing context for normalize functions
24
+ */
25
+ export interface RoutingContext {
26
+ parentChain?: Array<{ entity: string; id: string }>
27
+ path?: string
28
+ }
29
+
30
+ /**
31
+ * ApiStorage options
32
+ */
33
+ export interface ApiStorageOptions<T extends EntityRecord = EntityRecord> {
34
+ endpoint: string
35
+ client?: HttpClient | null
36
+ getClient?: (() => HttpClient) | null
37
+ responseItemsKey?: string
38
+ responseTotalKey?: string
39
+ paramMapping?: Record<string, string>
40
+ normalize?: ((data: T, context?: RoutingContext | null) => T) | null
41
+ denormalize?: ((data: Partial<T>) => Partial<T>) | null
42
+ }
43
+
44
+ /**
45
+ * ApiStorage - REST API storage adapter
46
+ *
47
+ * Implements the storage interface for REST APIs.
48
+ * Expects standard response format: { items: [], total: number, page: number }
49
+ */
50
+ export class ApiStorage<T extends EntityRecord = EntityRecord> extends IStorage<T> {
51
+ static storageName = 'ApiStorage'
52
+
53
+ static capabilities: StorageCapabilities = {
54
+ supportsTotal: true,
55
+ supportsFilters: true,
56
+ supportsPagination: true,
57
+ supportsCaching: true,
58
+ }
59
+
60
+ /** @deprecated Use static ApiStorage.capabilities.supportsCaching instead */
61
+ get supportsCaching(): boolean {
62
+ return ApiStorage.capabilities.supportsCaching
63
+ }
64
+
65
+ readonly endpoint: string
66
+ readonly responseItemsKey: string
67
+ readonly responseTotalKey: string
68
+ readonly paramMapping: Record<string, string>
69
+
70
+ protected _client: HttpClient | null
71
+ protected _getClient: (() => HttpClient) | null
72
+ protected _normalize: ((data: T, context?: RoutingContext | null) => T) | null
73
+ protected _denormalize: ((data: Partial<T>) => Partial<T>) | null
74
+
75
+ constructor(options: ApiStorageOptions<T>) {
76
+ super()
77
+ const {
78
+ endpoint,
79
+ client = null,
80
+ getClient = null,
81
+ responseItemsKey = 'items',
82
+ responseTotalKey = 'total',
83
+ paramMapping = {},
84
+ normalize = null,
85
+ denormalize = null,
86
+ } = options
87
+
88
+ this.endpoint = endpoint
89
+ this._client = client
90
+ this._getClient = getClient
91
+ this.responseItemsKey = responseItemsKey
92
+ this.responseTotalKey = responseTotalKey
93
+ this.paramMapping = paramMapping
94
+ this._normalize = normalize
95
+ this._denormalize = denormalize
96
+ }
97
+
98
+ /**
99
+ * Apply parameter mapping to transform filter names
100
+ */
101
+ protected _applyParamMapping(params: Record<string, unknown>): Record<string, unknown> {
102
+ if (!this.paramMapping || Object.keys(this.paramMapping).length === 0) {
103
+ return params
104
+ }
105
+ const mapped: Record<string, unknown> = {}
106
+ for (const [key, value] of Object.entries(params)) {
107
+ const mappedKey = this.paramMapping[key] || key
108
+ mapped[mappedKey] = value
109
+ }
110
+ return mapped
111
+ }
112
+
113
+ /**
114
+ * Normalize API response data to internal format
115
+ */
116
+ protected _normalizeData(data: T | T[], context: RoutingContext | null = null): T | T[] {
117
+ if (!this._normalize) return data
118
+ if (Array.isArray(data)) {
119
+ return data.map((item) => this._normalize!(item, context))
120
+ }
121
+ return this._normalize(data, context)
122
+ }
123
+
124
+ /**
125
+ * Denormalize internal data to API format
126
+ */
127
+ protected _denormalizeData(data: Partial<T>): Partial<T> {
128
+ if (!this._denormalize) return data
129
+ return this._denormalize(data)
130
+ }
131
+
132
+ get client(): HttpClient {
133
+ if (this._getClient) {
134
+ return this._getClient()
135
+ }
136
+ if (!this._client) {
137
+ throw new Error('ApiStorage: No HTTP client configured')
138
+ }
139
+ return this._client
140
+ }
141
+
142
+ set client(value: HttpClient) {
143
+ this._client = value
144
+ }
145
+
146
+ async list(params: ListParams = {}, context: RoutingContext | null = null): Promise<ListResult<T>> {
147
+ const { page = 1, page_size = 20, sort_by, sort_order, filters = {} } = params
148
+
149
+ const mappedFilters = this._applyParamMapping(filters)
150
+
151
+ const response = await this.client.get<Record<string, unknown>>(this.endpoint, {
152
+ params: { page, page_size, sort_by, sort_order, ...mappedFilters },
153
+ })
154
+
155
+ const data = response.data
156
+ const rawItems = (data[this.responseItemsKey] || data.items || data) as T[]
157
+ const items = this._normalizeData(rawItems, context) as T[]
158
+
159
+ return {
160
+ items,
161
+ total:
162
+ (data[this.responseTotalKey] as number) ||
163
+ (data.total as number) ||
164
+ (Array.isArray(data) ? data.length : 0),
165
+ }
166
+ }
167
+
168
+ async get(id: string | number, context: RoutingContext | null = null): Promise<T> {
169
+ const response = await this.client.get<T>(`${this.endpoint}/${id}`)
170
+ return this._normalizeData(response.data, context) as T
171
+ }
172
+
173
+ async create(data: Partial<T>): Promise<T> {
174
+ const apiData = this._denormalizeData(data)
175
+ const response = await this.client.post<T>(this.endpoint, apiData)
176
+ return this._normalizeData(response.data) as T
177
+ }
178
+
179
+ async update(id: string | number, data: Partial<T>): Promise<T> {
180
+ const apiData = this._denormalizeData(data)
181
+ const response = await this.client.put<T>(`${this.endpoint}/${id}`, apiData)
182
+ return this._normalizeData(response.data) as T
183
+ }
184
+
185
+ async patch(id: string | number, data: Partial<T>): Promise<T> {
186
+ const apiData = this._denormalizeData(data)
187
+ const response = await this.client.patch<T>(`${this.endpoint}/${id}`, apiData)
188
+ return this._normalizeData(response.data) as T
189
+ }
190
+
191
+ async delete(id: string | number): Promise<void> {
192
+ await this.client.delete(`${this.endpoint}/${id}`)
193
+ }
194
+
195
+ /**
196
+ * Generic request for special operations
197
+ */
198
+ async request<R = T>(
199
+ method: string,
200
+ endpoint: string,
201
+ options: {
202
+ data?: unknown
203
+ params?: Record<string, unknown>
204
+ headers?: Record<string, string>
205
+ context?: RoutingContext | null
206
+ } = {}
207
+ ): Promise<R> {
208
+ const { context, ...requestOptions } = options
209
+ const url = endpoint.startsWith('/') ? endpoint : `${this.endpoint}/${endpoint}`
210
+ const response = await this.client.request<R>({
211
+ method,
212
+ url,
213
+ data: requestOptions.data,
214
+ params: requestOptions.params,
215
+ headers: requestOptions.headers,
216
+ })
217
+ return this._normalizeData(response.data as unknown as T, context) as unknown as R
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Factory function to create an ApiStorage
223
+ */
224
+ export function createApiStorage<T extends EntityRecord = EntityRecord>(
225
+ options: ApiStorageOptions<T>
226
+ ): ApiStorage<T> {
227
+ return new ApiStorage(options)
228
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * IStorage - Base class for storage adapters
3
+ *
4
+ * Provides instanceof checking for storageFactory.
5
+ * All storage adapters should extend this class.
6
+ *
7
+ * Usage:
8
+ * ```ts
9
+ * class MyStorage extends IStorage<MyEntity> {
10
+ * static capabilities = { supportsTotal: true, ... }
11
+ * async list(params) { ... }
12
+ * async get(id) { ... }
13
+ * async create(data) { ... }
14
+ * async update(id, data) { ... }
15
+ * async delete(id) { ... }
16
+ * }
17
+ * ```
18
+ */
19
+
20
+ import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
21
+
22
+ /**
23
+ * Abstract base class for storage adapters
24
+ */
25
+ export abstract class IStorage<T extends EntityRecord = EntityRecord> {
26
+ /**
27
+ * Default capabilities - override in subclass
28
+ */
29
+ static capabilities: StorageCapabilities = {
30
+ supportsTotal: false,
31
+ supportsFilters: false,
32
+ supportsPagination: false,
33
+ supportsCaching: false,
34
+ }
35
+
36
+ /**
37
+ * List entities with optional filtering/pagination
38
+ * @param params - Query parameters
39
+ * @param _context - Optional context (e.g., parent entity)
40
+ * @returns Promise with items array and optional total count
41
+ */
42
+ async list(_params: ListParams = {}, _context?: unknown): Promise<ListResult<T>> {
43
+ throw new Error('list() not implemented')
44
+ }
45
+
46
+ /**
47
+ * Get single entity by ID
48
+ * @param _id - Entity ID
49
+ * @param _context - Optional context
50
+ * @returns Promise with entity or null if not found
51
+ */
52
+ async get(_id: string | number, _context?: unknown): Promise<T | null> {
53
+ throw new Error('get() not implemented')
54
+ }
55
+
56
+ /**
57
+ * Create new entity
58
+ * @param _data - Entity data
59
+ * @returns Promise with created entity including ID
60
+ */
61
+ async create(_data: Partial<T>): Promise<T> {
62
+ throw new Error('create() not implemented')
63
+ }
64
+
65
+ /**
66
+ * Update entity by ID (full replacement)
67
+ * @param _id - Entity ID
68
+ * @param _data - Updated data
69
+ * @returns Promise with updated entity
70
+ */
71
+ async update(_id: string | number, _data: Partial<T>): Promise<T> {
72
+ throw new Error('update() not implemented')
73
+ }
74
+
75
+ /**
76
+ * Patch entity by ID (partial update)
77
+ * @param _id - Entity ID
78
+ * @param _data - Partial data to merge
79
+ * @returns Promise with updated entity
80
+ */
81
+ async patch(_id: string | number, _data: Partial<T>): Promise<T> {
82
+ throw new Error('patch() not implemented')
83
+ }
84
+
85
+ /**
86
+ * Delete entity by ID
87
+ * @param _id - Entity ID
88
+ * @returns Promise that resolves when deleted
89
+ */
90
+ async delete(_id: string | number): Promise<void> {
91
+ throw new Error('delete() not implemented')
92
+ }
93
+
94
+ /**
95
+ * Reset storage state (for testing/development)
96
+ */
97
+ reset(): void {
98
+ // Default no-op, override if needed
99
+ }
100
+ }