@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,944 @@
1
+ import { PermissiveAuthAdapter } from './auth/PermissiveAdapter'
2
+ import { AuthActions, type AuthUser } from './auth/EntityAuthAdapter'
3
+ import type { EntityAuthAdapter as IEntityAuthAdapter } from './auth/EntityAuthAdapter'
4
+ import pluralize from 'pluralize'
5
+ import { applyCacheMethods } from './EntityManager.cache'
6
+ import { applyQueryMethods } from './EntityManager.query'
7
+ import { applyRelationsMethods } from './EntityManager.relations'
8
+ import { applyCrudMethods } from './EntityManager.crud'
9
+ import type {
10
+ EntityRecord,
11
+ ListParams,
12
+ ListResult,
13
+ IStorage,
14
+ FieldConfig,
15
+ ChildConfig,
16
+ ParentConfig,
17
+ RelationConfig,
18
+ NavConfig,
19
+ } from '../types'
20
+ import type { SignalBus } from '../kernel/SignalBus'
21
+ import type { HookRegistry } from '../hooks/HookRegistry'
22
+ import type { QueryObject } from '../query/QueryExecutor'
23
+
24
+ // Import types from dedicated types file
25
+ import type {
26
+ CacheState,
27
+ DetailCacheState,
28
+ OperationStats,
29
+ ResolvedStorage,
30
+ StorageResolution,
31
+ Orchestrator,
32
+ } from './EntityManager.types'
33
+
34
+ // Re-export public types from types file
35
+ export type {
36
+ RoutingContext,
37
+ PresaveContext,
38
+ PostsaveContext,
39
+ PredeleteContext,
40
+ QueryOptions,
41
+ WhitelistContext,
42
+ CacheInfo,
43
+ EntityBadge,
44
+ SeverityDescriptor,
45
+ SeverityMapValue,
46
+ SeverityMap,
47
+ EntityManagerOptions,
48
+ } from './EntityManager.types'
49
+
50
+ // Import concrete types needed in method signatures
51
+ import type {
52
+ RoutingContext,
53
+ PresaveContext,
54
+ PostsaveContext,
55
+ PredeleteContext,
56
+ QueryOptions,
57
+ WhitelistContext,
58
+ CacheInfo,
59
+ EntityBadge,
60
+ SeverityMap,
61
+ SeverityDescriptor,
62
+ EntityManagerOptions,
63
+ } from './EntityManager.types'
64
+
65
+ /**
66
+ * EntityManager - Base class for entity CRUD operations
67
+ *
68
+ * An EntityManager can either:
69
+ * 1. BE its own storage (implement methods directly)
70
+ * 2. DELEGATE to a storage adapter
71
+ */
72
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
73
+ export class EntityManager<T extends EntityRecord = EntityRecord> {
74
+ readonly name: string
75
+ storage: IStorage<T> | null
76
+ readonly idField: string
77
+
78
+ protected _labelField: string | ((entity: T) => string)
79
+ protected _badges: ((entity: T) => EntityBadge[]) | null
80
+ protected _label: string | null
81
+ protected _labelPlural: string | null
82
+ protected _routePrefix: string | null
83
+ protected _fields: Record<string, FieldConfig>
84
+
85
+ localFilterThreshold: number | null
86
+ protected _cacheTtlMs: number | null
87
+ protected _asymmetric: boolean
88
+ protected _detailCache: DetailCacheState<T> = { items: new Map() }
89
+ protected _detailCacheTtlMs: number
90
+ protected _detailCacheMaxSize: number
91
+ protected _detailInflight: Map<string, Promise<T>> = new Map()
92
+ protected _readOnly: boolean
93
+ protected _warmup: boolean
94
+ protected _authSensitive: boolean
95
+ protected _system: boolean
96
+
97
+ protected _scopeWhitelist: string[] | null
98
+ protected _isOwn: ((record: T, user: AuthUser) => boolean) | null
99
+
100
+ protected _children: Record<string, ChildConfig>
101
+ protected _parent: ParentConfig | null
102
+ protected _parents: Record<string, ParentConfig>
103
+ protected _relations: Record<string, RelationConfig>
104
+ protected _orchestrator: Orchestrator | null = null
105
+
106
+ protected _authAdapter: IEntityAuthAdapter | null
107
+ protected _nav: NavConfig
108
+
109
+ protected _hooks: HookRegistry | null = null
110
+ protected _severityMaps: Record<string, SeverityMap> = {}
111
+
112
+ protected _cache: CacheState<T> = {
113
+ items: [],
114
+ total: 0,
115
+ loadedAt: null,
116
+ valid: false,
117
+ }
118
+ protected _cacheLoading: Promise<boolean> | null = null
119
+
120
+ protected _signals: SignalBus | null = null
121
+ protected _signalCleanup: (() => void) | null = null
122
+
123
+ protected _stats: OperationStats = {
124
+ list: 0,
125
+ get: 0,
126
+ create: 0,
127
+ update: 0,
128
+ delete: 0,
129
+ cacheHits: 0,
130
+ cacheMisses: 0,
131
+ detailCacheHits: 0,
132
+ detailCacheMisses: 0,
133
+ maxItemsSeen: 0,
134
+ maxTotal: 0,
135
+ }
136
+
137
+ // Hook for query result customization (can be overridden)
138
+ onQueryResult?: (
139
+ result: ListResult<T>,
140
+ context: WhitelistContext
141
+ ) => ListResult<T> | void
142
+
143
+ constructor(options: EntityManagerOptions<T> = {} as EntityManagerOptions<T>) {
144
+ const {
145
+ name,
146
+ storage = null,
147
+ idField = 'id',
148
+ label = null,
149
+ labelPlural = null,
150
+ routePrefix = null,
151
+ labelField = 'name',
152
+ badges = null,
153
+ fields = {},
154
+ localFilterThreshold = null,
155
+ cacheTtlMs = null,
156
+ asymmetric = false,
157
+ detailCacheTtlMs = 0,
158
+ detailCacheMaxSize = 0,
159
+ readOnly = false,
160
+ warmup = true,
161
+ authSensitive,
162
+ system = false,
163
+ scopeWhitelist = null,
164
+ isOwn = null,
165
+ children = {},
166
+ parent = null,
167
+ parents = {},
168
+ relations = {},
169
+ authAdapter = null,
170
+ nav = {},
171
+ } = options
172
+
173
+ this.name = name
174
+ this.storage = storage
175
+ this.idField = idField
176
+ this._labelField = labelField
177
+ this._badges = badges
178
+
179
+ this._label = label
180
+ this._labelPlural = labelPlural
181
+ this._routePrefix = routePrefix
182
+ this._fields = fields
183
+
184
+ this.localFilterThreshold = localFilterThreshold
185
+ this._cacheTtlMs = cacheTtlMs
186
+ this._asymmetric = asymmetric
187
+ this._detailCacheTtlMs = detailCacheTtlMs
188
+ this._detailCacheMaxSize = detailCacheMaxSize
189
+ this._readOnly = readOnly
190
+ this._warmup = warmup
191
+ this._authSensitive = authSensitive ?? this._getStorageRequiresAuth()
192
+ this._system = system
193
+
194
+ this._scopeWhitelist = scopeWhitelist
195
+ this._isOwn = isOwn
196
+
197
+ this._children = children
198
+ this._parent = parent
199
+ this._parents = parents
200
+ this._relations = relations
201
+
202
+ this._authAdapter = authAdapter
203
+ this._nav = nav
204
+ }
205
+
206
+ // ============ SIGNALS ============
207
+
208
+ /**
209
+ * Set the SignalBus reference (called by Orchestrator during registration)
210
+ */
211
+ setSignals(signals: SignalBus): void {
212
+ this._signals = signals
213
+ this._setupCacheListeners()
214
+ }
215
+
216
+ /**
217
+ * Set the HookRegistry reference (called by Orchestrator during registration)
218
+ */
219
+ setHooks(hooks: HookRegistry): void {
220
+ this._hooks = hooks
221
+ }
222
+
223
+ /**
224
+ * Get the HookRegistry reference
225
+ */
226
+ get hooks(): HookRegistry | null {
227
+ return this._hooks
228
+ }
229
+
230
+ /**
231
+ * Emit entity signals after CRUD operations
232
+ */
233
+ protected _emitSignal(
234
+ action: 'created' | 'updated' | 'deleted',
235
+ data: Record<string, unknown>
236
+ ): void {
237
+ if (!this._signals) return
238
+ this._signals.emitEntity(this.name, action, data)
239
+ }
240
+
241
+ /**
242
+ * Emit entity:data-invalidate signal for client cache invalidation
243
+ */
244
+ protected _emitDataInvalidate(
245
+ action: 'created' | 'updated' | 'deleted',
246
+ id: string | number | undefined
247
+ ): void {
248
+ if (!this._signals) return
249
+ this._signals.emit('entity:data-invalidate', {
250
+ entity: this.name,
251
+ action,
252
+ id,
253
+ })
254
+ }
255
+
256
+ // ============ LIFECYCLE HOOKS ============
257
+
258
+ /**
259
+ * Invoke a lifecycle hook for this entity
260
+ */
261
+ protected async _invokeHook(
262
+ hookName: string,
263
+ context: Record<string, unknown>
264
+ ): Promise<void> {
265
+ if (!this._hooks) return
266
+ await this._hooks.invoke(`entity:${hookName}`, context)
267
+ }
268
+
269
+ /**
270
+ * Build hook context for presave operations
271
+ */
272
+ protected _buildPresaveContext(
273
+ data: Partial<T>,
274
+ isNew: boolean,
275
+ id: string | number | null = null
276
+ ): PresaveContext<T> {
277
+ const context: PresaveContext<T> = {
278
+ entity: this.name,
279
+ record: data,
280
+ isNew,
281
+ manager: this,
282
+ }
283
+ if (!isNew && id !== null) {
284
+ context.id = id
285
+ }
286
+ return context
287
+ }
288
+
289
+ /**
290
+ * Build hook context for postsave operations
291
+ */
292
+ protected _buildPostsaveContext(
293
+ data: Partial<T>,
294
+ result: T,
295
+ isNew: boolean,
296
+ id: string | number | null = null
297
+ ): PostsaveContext<T> {
298
+ const context: PostsaveContext<T> = {
299
+ entity: this.name,
300
+ record: data,
301
+ result,
302
+ isNew,
303
+ manager: this,
304
+ }
305
+ if (id !== null) {
306
+ context.id = id
307
+ } else if (result?.[this.idField] !== undefined) {
308
+ context.id = result[this.idField] as string | number
309
+ }
310
+ return context
311
+ }
312
+
313
+ /**
314
+ * Build hook context for predelete operations
315
+ */
316
+ protected _buildPredeleteContext(id: string | number): PredeleteContext<T> {
317
+ return {
318
+ entity: this.name,
319
+ id,
320
+ manager: this,
321
+ }
322
+ }
323
+
324
+ // ============ STORAGE RESOLUTION ============
325
+
326
+ /**
327
+ * Resolve which storage to use for an operation
328
+ */
329
+ resolveStorage(
330
+ _method: string,
331
+ _context?: RoutingContext
332
+ ): StorageResolution<T> {
333
+ return { storage: this.storage }
334
+ }
335
+
336
+ /**
337
+ * Normalize resolveStorage() return value to standard format
338
+ */
339
+ protected _normalizeResolveResult(
340
+ result: StorageResolution<T>,
341
+ context?: RoutingContext
342
+ ): ResolvedStorage<T> {
343
+ // Function = dynamic endpoint builder
344
+ if (typeof result === 'function') {
345
+ return {
346
+ storage: this.storage,
347
+ endpoint: result(context),
348
+ isDynamic: true,
349
+ }
350
+ }
351
+
352
+ // String = endpoint with primary storage
353
+ if (typeof result === 'string') {
354
+ return { storage: this.storage, endpoint: result }
355
+ }
356
+
357
+ // Null/undefined = primary storage
358
+ if (!result) {
359
+ return { storage: this.storage }
360
+ }
361
+
362
+ // Object with endpoint function = dynamic endpoint builder
363
+ if (typeof result.endpoint === 'function') {
364
+ return {
365
+ storage: result.storage || this.storage,
366
+ endpoint: result.endpoint(context),
367
+ params: result.params,
368
+ isDynamic: true,
369
+ }
370
+ }
371
+
372
+ // Object without storage = use primary storage
373
+ if (!result.storage) {
374
+ return {
375
+ storage: this.storage,
376
+ endpoint: result.endpoint as string | undefined,
377
+ params: result.params,
378
+ }
379
+ }
380
+
381
+ return {
382
+ storage: result.storage,
383
+ endpoint: result.endpoint as string | undefined,
384
+ params: result.params,
385
+ }
386
+ }
387
+
388
+ // ============ METADATA ACCESSORS ============
389
+
390
+ /**
391
+ * Get entity label (singular)
392
+ */
393
+ get label(): string {
394
+ if (this._label) return this._label
395
+ if (!this.name) return 'Item'
396
+ const singular = pluralize.singular(this.name)
397
+ return singular.charAt(0).toUpperCase() + singular.slice(1)
398
+ }
399
+
400
+ /**
401
+ * Get entity label (plural)
402
+ */
403
+ get labelPlural(): string {
404
+ if (this._labelPlural) return this._labelPlural
405
+ if (!this.name) return 'Items'
406
+ const plural = pluralize.plural(this.name)
407
+ return plural.charAt(0).toUpperCase() + plural.slice(1)
408
+ }
409
+
410
+ /**
411
+ * Get route prefix for this entity
412
+ */
413
+ get routePrefix(): string {
414
+ if (this._routePrefix) return this._routePrefix
415
+ if (!this.name) return 'item'
416
+ return pluralize.singular(this.name)
417
+ }
418
+
419
+ /**
420
+ * Get labelField config (string or function)
421
+ */
422
+ get labelField(): string | ((entity: T) => string) {
423
+ return this._labelField
424
+ }
425
+
426
+ /**
427
+ * Get display label for an entity
428
+ */
429
+ getEntityLabel(entity: T | null): string | null {
430
+ if (!entity) return null
431
+ if (typeof this._labelField === 'function') {
432
+ return this._labelField(entity)
433
+ }
434
+ return (entity[this._labelField] as string) || null
435
+ }
436
+
437
+ /**
438
+ * Get badges for an entity item header
439
+ */
440
+ getEntityBadges(entity: T | null): EntityBadge[] {
441
+ if (!entity || !this._badges) return []
442
+ return this._badges(entity)
443
+ }
444
+
445
+ /**
446
+ * Get navigation config for auto-generated menus
447
+ */
448
+ get nav(): NavConfig {
449
+ return this._nav || {}
450
+ }
451
+
452
+ // ============ PERMISSIONS ============
453
+
454
+ /**
455
+ * Get the auth adapter (lazy-initialized PermissiveAuthAdapter if not set)
456
+ */
457
+ get authAdapter(): IEntityAuthAdapter {
458
+ if (!this._authAdapter) {
459
+ this._authAdapter = new PermissiveAuthAdapter()
460
+ }
461
+ return this._authAdapter
462
+ }
463
+
464
+ /**
465
+ * Set the auth adapter
466
+ */
467
+ set authAdapter(adapter: IEntityAuthAdapter | null) {
468
+ this._authAdapter = adapter
469
+ }
470
+
471
+ /**
472
+ * Build permission string for an entity action
473
+ */
474
+ protected _getPermissionString(action: string): string {
475
+ return `entity:${this.name}:${action}`
476
+ }
477
+
478
+ /**
479
+ * Get the current authenticated user
480
+ */
481
+ protected _getCurrentUser(): AuthUser | null {
482
+ // Try authAdapter.getCurrentUser() first (EntityAuthAdapter subclass)
483
+ if (typeof this.authAdapter?.getCurrentUser === 'function') {
484
+ try {
485
+ return this.authAdapter.getCurrentUser() ?? null
486
+ } catch {
487
+ // Fallback if not implemented
488
+ }
489
+ }
490
+ // Fallback to kernel's authAdapter
491
+ return this._orchestrator?.kernel?.options?.authAdapter?.getUser?.() ?? null
492
+ }
493
+
494
+ /**
495
+ * Check if SecurityChecker is configured via authAdapter
496
+ */
497
+ protected _hasSecurityChecker(): boolean {
498
+ return (
499
+ (this.authAdapter as unknown as { _securityChecker?: unknown })
500
+ ?._securityChecker != null
501
+ )
502
+ }
503
+
504
+ /**
505
+ * Check if the current user can perform an action, optionally on a specific record
506
+ */
507
+ canAccess(action: string, record: T | null = null): boolean {
508
+ // 1. Check readOnly restriction for write actions
509
+ if (
510
+ this._readOnly &&
511
+ action !== AuthActions.READ &&
512
+ action !== AuthActions.LIST
513
+ ) {
514
+ return false
515
+ }
516
+
517
+ // 2. Ownership check: if user owns the record, check entity-own permission
518
+ if (record && this._isOwn && this._hasSecurityChecker()) {
519
+ const user = this._getCurrentUser()
520
+ if (user && this._isOwn(record, user)) {
521
+ // Owner - check entity-own:{entity}:{action} permission
522
+ const ownPerm = `entity-own:${this.name}:${action}`
523
+ if (this.authAdapter.isGranted?.(ownPerm, record)) {
524
+ return true
525
+ }
526
+ }
527
+ }
528
+
529
+ // 3. Use isGranted() with entity:name:action format when available
530
+ if (this._hasSecurityChecker()) {
531
+ const perm = this._getPermissionString(action)
532
+ return this.authAdapter.isGranted?.(perm, record) ?? false
533
+ }
534
+
535
+ // 4. Legacy fallback: canPerform() + canAccessRecord()
536
+ const canPerformAction = this.authAdapter.canPerform(this.name, action)
537
+ if (!canPerformAction) {
538
+ return false
539
+ }
540
+
541
+ // 5. Silo check: if record provided, can user access this specific record?
542
+ if (record !== null) {
543
+ return this.authAdapter.canAccessRecord?.(this.name, record) ?? true
544
+ }
545
+
546
+ return true
547
+ }
548
+
549
+ /**
550
+ * Check if user can read entities
551
+ */
552
+ canRead(entity: T | null = null): boolean {
553
+ return this.canAccess(AuthActions.READ, entity)
554
+ }
555
+
556
+ /**
557
+ * Check if manager is read-only
558
+ */
559
+ get readOnly(): boolean {
560
+ return this._readOnly
561
+ }
562
+
563
+ /**
564
+ * Check if entity is system-provided (roles, users)
565
+ */
566
+ get system(): boolean {
567
+ return this._system
568
+ }
569
+
570
+ /**
571
+ * Check if user can create new entities
572
+ */
573
+ canCreate(): boolean {
574
+ return this.canAccess(AuthActions.CREATE)
575
+ }
576
+
577
+ /**
578
+ * Check if user can update entities
579
+ */
580
+ canUpdate(entity: T | null = null): boolean {
581
+ return this.canAccess(AuthActions.UPDATE, entity)
582
+ }
583
+
584
+ /**
585
+ * Check if user can delete entities
586
+ */
587
+ canDelete(entity: T | null = null): boolean {
588
+ return this.canAccess(AuthActions.DELETE, entity)
589
+ }
590
+
591
+ /**
592
+ * Check if user can list entities
593
+ */
594
+ canList(): boolean {
595
+ return this.canAccess(AuthActions.LIST)
596
+ }
597
+
598
+ /**
599
+ * Get scope whitelist
600
+ */
601
+ get scopeWhitelist(): string[] | null {
602
+ return this._scopeWhitelist
603
+ }
604
+
605
+ /**
606
+ * Check if a context is whitelisted (can bypass restrictions)
607
+ */
608
+ isWhitelisted(context: WhitelistContext = {}): boolean {
609
+ // Explicit bypass with reason
610
+ if (context.bypassPermissions && context.reason) {
611
+ return true
612
+ }
613
+
614
+ // No whitelist configured = nothing whitelisted
615
+ if (!this._scopeWhitelist || this._scopeWhitelist.length === 0) {
616
+ return false
617
+ }
618
+
619
+ // Check if any context property matches the whitelist
620
+ const checkValues = [context.module, context.scope, context.form].filter(
621
+ Boolean
622
+ ) as string[]
623
+ return checkValues.some((val) => this._scopeWhitelist!.includes(val))
624
+ }
625
+
626
+ /**
627
+ * Get fields schema
628
+ */
629
+ get fields(): Record<string, FieldConfig> {
630
+ return this._fields
631
+ }
632
+
633
+ /**
634
+ * Get a specific field config
635
+ */
636
+ getFieldConfig(fieldName: string): FieldConfig | undefined {
637
+ return this._fields[fieldName]
638
+ }
639
+
640
+ /**
641
+ * Get initial data for a new entity based on field defaults
642
+ */
643
+ getInitialData(context: RoutingContext | null = null): Partial<T> {
644
+ const data: Record<string, unknown> = {}
645
+ for (const [fieldName, fieldConfig] of Object.entries(this._fields)) {
646
+ if (fieldConfig.default !== undefined) {
647
+ data[fieldName] =
648
+ typeof fieldConfig.default === 'function'
649
+ ? (fieldConfig.default as (ctx: RoutingContext | null) => unknown)(
650
+ context
651
+ )
652
+ : fieldConfig.default
653
+ } else {
654
+ // Type-based defaults
655
+ switch (fieldConfig.type) {
656
+ case 'boolean':
657
+ data[fieldName] = false
658
+ break
659
+ case 'number':
660
+ data[fieldName] = null
661
+ break
662
+ case 'array':
663
+ data[fieldName] = []
664
+ break
665
+ case 'object':
666
+ data[fieldName] = {}
667
+ break
668
+ default:
669
+ data[fieldName] = ''
670
+ }
671
+ }
672
+ }
673
+ return data as Partial<T>
674
+ }
675
+
676
+ /**
677
+ * Apply field defaults to data (for create operations)
678
+ */
679
+ applyDefaults(
680
+ data: Partial<T>,
681
+ context: RoutingContext | null = null
682
+ ): Partial<T> {
683
+ const result = { ...data } as Record<string, unknown>
684
+ for (const [fieldName, fieldConfig] of Object.entries(this._fields)) {
685
+ // Only apply default if field is undefined in data
686
+ if (
687
+ result[fieldName] === undefined &&
688
+ fieldConfig.default !== undefined
689
+ ) {
690
+ result[fieldName] =
691
+ typeof fieldConfig.default === 'function'
692
+ ? (fieldConfig.default as (ctx: RoutingContext | null) => unknown)(
693
+ context
694
+ )
695
+ : fieldConfig.default
696
+ }
697
+ }
698
+ return result as Partial<T>
699
+ }
700
+
701
+ /**
702
+ * Get field names that are required
703
+ */
704
+ getRequiredFields(): string[] {
705
+ return Object.entries(this._fields)
706
+ .filter(([, config]) => config.required)
707
+ .map(([name]) => name)
708
+ }
709
+
710
+ /**
711
+ * Get fields that should appear in list view
712
+ */
713
+ getListFields(): Array<{ name: string } & FieldConfig> {
714
+ return Object.entries(this._fields)
715
+ .filter(([, config]) => config.listable !== false)
716
+ .map(([name, config]) => ({ name, ...config }))
717
+ }
718
+
719
+ /**
720
+ * Get fields that should appear in form view
721
+ */
722
+ getFormFields(): Array<{ name: string } & FieldConfig> {
723
+ return Object.entries(this._fields)
724
+ .filter(([, config]) => config.editable !== false)
725
+ .map(([name, config]) => ({ name, ...config }))
726
+ }
727
+
728
+ // ============ REFERENCE OPTIONS ============
729
+
730
+ /**
731
+ * Resolve reference options for a field
732
+ */
733
+ async resolveReferenceOptions(
734
+ fieldName: string
735
+ ): Promise<Array<{ label: string; value: unknown }>> {
736
+ const fieldConfig = this._fields[fieldName]
737
+ if (!fieldConfig) {
738
+ console.warn(`[EntityManager:${this.name}] Unknown field '${fieldName}'`)
739
+ return []
740
+ }
741
+
742
+ // If field has static options, return them
743
+ if (fieldConfig.options && !fieldConfig.reference) {
744
+ return fieldConfig.options
745
+ }
746
+
747
+ // If no reference, return empty
748
+ if (!fieldConfig.reference) {
749
+ return []
750
+ }
751
+
752
+ // Need orchestrator to access other managers
753
+ if (!this._orchestrator) {
754
+ console.warn(
755
+ `[EntityManager:${this.name}] No orchestrator, cannot resolve reference for '${fieldName}'`
756
+ )
757
+ return fieldConfig.options || []
758
+ }
759
+
760
+ const { entity, labelField, valueField } = fieldConfig.reference
761
+ const refManager = this._orchestrator.get(entity)
762
+
763
+ if (!refManager) {
764
+ console.warn(
765
+ `[EntityManager:${this.name}] Referenced entity '${entity}' not found`
766
+ )
767
+ return fieldConfig.options || []
768
+ }
769
+
770
+ try {
771
+ // Fetch all items from referenced entity
772
+ const { items } = await refManager.list({ page_size: 1000 })
773
+
774
+ // Build options array
775
+ const refLabelField = labelField || refManager.labelField || 'label'
776
+ const refValueField = valueField || refManager.idField || 'id'
777
+
778
+ return items.map((item) => ({
779
+ label:
780
+ (item[refLabelField as keyof typeof item] as string) ??
781
+ (item[refValueField as keyof typeof item] as string),
782
+ value: item[refValueField as keyof typeof item],
783
+ }))
784
+ } catch (error) {
785
+ console.error(
786
+ `[EntityManager:${this.name}] Failed to resolve reference for '${fieldName}':`,
787
+ error
788
+ )
789
+ return fieldConfig.options || []
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Resolve all reference options for form fields
795
+ */
796
+ async resolveAllReferenceOptions(): Promise<
797
+ Map<string, Array<{ label: string; value: unknown }>>
798
+ > {
799
+ const optionsMap = new Map<string, Array<{ label: string; value: unknown }>>()
800
+
801
+ for (const [fieldName, fieldConfig] of Object.entries(this._fields)) {
802
+ if (fieldConfig.reference) {
803
+ const options = await this.resolveReferenceOptions(fieldName)
804
+ optionsMap.set(fieldName, options)
805
+ }
806
+ }
807
+
808
+ return optionsMap
809
+ }
810
+
811
+ // ============ SEVERITY MAPS ============
812
+
813
+ /**
814
+ * Set severity map for a field.
815
+ * Values can be plain severity strings or rich descriptors with icon/label.
816
+ */
817
+ setSeverityMap(field: string, map: SeverityMap): this {
818
+ this._severityMaps[field] = map
819
+ return this
820
+ }
821
+
822
+ /**
823
+ * Get severity string for a field value (backward compat).
824
+ * Extracts .severity from descriptors automatically.
825
+ */
826
+ getSeverity(
827
+ field: string,
828
+ value: string | number,
829
+ defaultSeverity: string = 'secondary'
830
+ ): string {
831
+ const entry = this._severityMaps[field]?.[String(value)]
832
+ if (!entry) return defaultSeverity
833
+ return typeof entry === 'string' ? entry : entry.severity
834
+ }
835
+
836
+ /**
837
+ * Get full severity descriptor for a field value.
838
+ * Returns normalized descriptor (plain strings wrapped as { severity }).
839
+ */
840
+ getSeverityDescriptor(
841
+ field: string,
842
+ value: string | number,
843
+ defaultSeverity: string = 'secondary'
844
+ ): SeverityDescriptor {
845
+ const entry = this._severityMaps[field]?.[String(value)]
846
+ if (!entry) return { severity: defaultSeverity }
847
+ return typeof entry === 'string' ? { severity: entry } : entry
848
+ }
849
+
850
+ /**
851
+ * Check if a severity map exists for a field
852
+ */
853
+ hasSeverityMap(field: string): boolean {
854
+ return field in this._severityMaps
855
+ }
856
+
857
+ /**
858
+ * Hook: called when orchestrator is disposed
859
+ */
860
+ onDispose(): void {
861
+ // Override in subclass if needed
862
+ }
863
+ }
864
+
865
+ // ============ INTERFACE MERGING ============
866
+ // Declares prototype-patched methods for type safety.
867
+
868
+ export interface EntityManager<T extends EntityRecord = EntityRecord> {
869
+ // --- Cache getters ---
870
+ readonly effectiveThreshold: number
871
+ readonly effectiveCacheTtlMs: number
872
+ readonly isCacheTtlDisabled: boolean
873
+ readonly storageSupportsTotal: boolean
874
+ readonly storageSearchFields: string[] | undefined
875
+ readonly isCacheEnabled: boolean
876
+ readonly overflow: boolean
877
+ readonly warmupEnabled: boolean
878
+ readonly isAsymmetric: boolean
879
+ readonly effectiveDetailCacheTtlMs: number
880
+ readonly effectiveDetailCacheMaxSize: number
881
+ readonly isDetailCacheEnabled: boolean
882
+
883
+ // --- Cache methods ---
884
+ /** @internal */ _isCacheExpired(): boolean
885
+ /** @internal */ _isDetailCacheEntryExpired(loadedAt: number): boolean
886
+ /** @internal */ _evictDetailCache(): void
887
+ invalidateDetailCache(): void
888
+ /** @internal */ _getStorageRequiresAuth(): boolean
889
+ /** @internal */ _parseSearchFields(overrideSearchFields?: string[] | null): { ownFields: string[]; parentFields: Record<string, string[]> }
890
+ /** @internal */ _resolveSearchFields(items: T[]): Promise<void>
891
+ /** @internal */ _setupCacheListeners(): void
892
+ /** @internal */ _clearSearchCache(): void
893
+ /** @internal */ _loadCacheInBackground(): void
894
+ /** @internal */ _loadCache(): Promise<boolean>
895
+ invalidateCache(): void
896
+ invalidateDataLayer(): void
897
+ ensureCache(): Promise<boolean>
898
+ warmup(): Promise<boolean | null>
899
+ getCacheInfo(): CacheInfo
900
+
901
+ // --- Query methods ---
902
+ /** @internal */ _buildQuery(filters: Record<string, unknown>): QueryObject
903
+ /** @internal */ _filterLocally(items: T[], params?: ListParams & { searchFields?: string[] | null }): { items: T[]; total: number }
904
+ query(params?: ListParams, options?: QueryOptions): Promise<ListResult<T>>
905
+ getStats(): OperationStats
906
+ resetStats(): void
907
+
908
+ // --- CRUD methods ---
909
+ list(params?: ListParams & { _internal?: boolean }, context?: RoutingContext): Promise<ListResult<T>>
910
+ get(id: string | number, context?: RoutingContext): Promise<T>
911
+ getMany(ids: Array<string | number>, context?: RoutingContext): Promise<T[]>
912
+ create(data: Partial<T>, context?: RoutingContext): Promise<T>
913
+ update(id: string | number, data: Partial<T>, context?: RoutingContext): Promise<T>
914
+ patch(id: string | number, data: Partial<T>, context?: RoutingContext): Promise<T>
915
+ delete(id: string | number, context?: RoutingContext): Promise<void>
916
+ request(method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: string, options?: { data?: unknown; params?: Record<string, unknown>; headers?: Record<string, string>; invalidateCache?: boolean }): Promise<unknown>
917
+ onRegister(orchestrator: Orchestrator): void
918
+
919
+ // --- Relations methods ---
920
+ getChildConfig(childName: string): ChildConfig | undefined
921
+ getChildNames(): string[]
922
+ getParentConfig(): ParentConfig | null
923
+ listChildren(parentId: string | number, childName: string, params?: ListParams): Promise<ListResult<EntityRecord>>
924
+ getChild(parentId: string | number, childName: string, childId: string | number): Promise<EntityRecord>
925
+ createChild(parentId: string | number, childName: string, data: Record<string, unknown>): Promise<EntityRecord>
926
+ deleteChild(parentId: string | number, childName: string, childId: string | number): Promise<void>
927
+ getParentManager(): EntityManager<EntityRecord> | null
928
+ getChildManager(childName: string): EntityManager<EntityRecord> | null
929
+ }
930
+
931
+ // ============ APPLY PROTOTYPE PATCHES ============
932
+ applyCacheMethods(EntityManager)
933
+ applyQueryMethods(EntityManager)
934
+ applyRelationsMethods(EntityManager)
935
+ applyCrudMethods(EntityManager)
936
+
937
+ /**
938
+ * Factory function to create an EntityManager
939
+ */
940
+ export function createEntityManager<T extends EntityRecord = EntityRecord>(
941
+ options: EntityManagerOptions<T>
942
+ ): EntityManager<T> {
943
+ return new EntityManager<T>(options)
944
+ }