@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,221 @@
1
+ import { QueryExecutor, type QueryObject } from '../query/QueryExecutor'
2
+ import type { ListParams, ListResult } from '../types'
3
+ import type { EntityManagerInternal, OperationStats, QueryOptions } from './EntityManager.types'
4
+
5
+ type Self = EntityManagerInternal<any>
6
+
7
+ /**
8
+ * Patch EntityManager prototype with query-related methods.
9
+ */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export function applyQueryMethods(EntityManagerClass: { prototype: any }): void {
12
+ const proto = EntityManagerClass.prototype as Self
13
+
14
+ /**
15
+ * Query entities with automatic cache/API decision
16
+ */
17
+ proto.query = async function (
18
+ this: Self,
19
+ params: ListParams = {},
20
+ options: QueryOptions = {}
21
+ ): Promise<ListResult<any>> {
22
+ const { context = {}, routingContext = null } = options
23
+
24
+ // Check if this context uses a dynamic endpoint (no caching possible)
25
+ const resolved = this._normalizeResolveResult(
26
+ this.resolveStorage('list', routingContext ?? undefined),
27
+ routingContext ?? undefined
28
+ )
29
+ const isDynamicEndpoint = !!resolved.endpoint
30
+
31
+ // Ensure cache is filled (via list) — skip for dynamic endpoints
32
+ // since list() disables caching when resolveStorage provides an endpoint
33
+ const cache = this._cache
34
+ if (!isDynamicEndpoint && !cache.valid && this.isCacheEnabled) {
35
+ await this.list(
36
+ { page_size: this.effectiveThreshold },
37
+ routingContext ?? undefined
38
+ )
39
+ }
40
+
41
+ let result: ListResult<any>
42
+
43
+ // Dynamic endpoint, overflow, or cache disabled → use API directly
44
+ if (isDynamicEndpoint || this.overflow || !this.isCacheEnabled) {
45
+ result = await this.list(params, routingContext ?? undefined)
46
+ } else {
47
+ // Full cache available - filter locally
48
+ const filtered = this._filterLocally(cache.items, params)
49
+ result = { ...filtered, fromCache: true }
50
+ }
51
+
52
+ // Call hook if defined (for context-based customization)
53
+ if (this.onQueryResult) {
54
+ result = this.onQueryResult(result, context) || result
55
+ }
56
+
57
+ return result
58
+ }
59
+
60
+ /**
61
+ * Build MongoDB-like query from filters object
62
+ * @internal
63
+ */
64
+ proto._buildQuery = function (
65
+ this: Self,
66
+ filters: Record<string, unknown>
67
+ ): QueryObject {
68
+ const query: QueryObject = {}
69
+ for (const [field, value] of Object.entries(filters)) {
70
+ if (value === null || value === undefined || value === '') continue
71
+ // Cast value to QueryCondition - QueryExecutor handles type validation
72
+ query[field] = value as QueryObject[string]
73
+ }
74
+ return query
75
+ }
76
+
77
+ /**
78
+ * Apply filters, search, sort and pagination locally
79
+ * @internal
80
+ */
81
+ proto._filterLocally = function (
82
+ this: Self,
83
+ items: any[],
84
+ params: ListParams & { searchFields?: string[] | null } = {}
85
+ ): { items: any[]; total: number } {
86
+ const {
87
+ search = '',
88
+ searchFields: overrideSearchFields = null,
89
+ filters = {},
90
+ sort_by = null,
91
+ sort_order = 'asc',
92
+ page = 1,
93
+ page_size = 20,
94
+ } = params
95
+
96
+ let result = [...items]
97
+
98
+ // Apply search
99
+ if (search) {
100
+ const searchLower = search.toLowerCase()
101
+ const searchFields = overrideSearchFields ?? this.storageSearchFields
102
+
103
+ result = result.filter((item: any) => {
104
+ if (searchFields) {
105
+ const { ownFields, parentFields } =
106
+ this._parseSearchFields(searchFields)
107
+
108
+ // Search own fields
109
+ for (const field of ownFields) {
110
+ const value = item[field]
111
+ if (
112
+ typeof value === 'string' &&
113
+ value.toLowerCase().includes(searchLower)
114
+ ) {
115
+ return true
116
+ }
117
+ if (
118
+ typeof value === 'number' &&
119
+ value.toString().includes(search)
120
+ ) {
121
+ return true
122
+ }
123
+ }
124
+
125
+ // Search cached parent fields (in item._search)
126
+ const itemWithSearch = item as {
127
+ _search?: Record<string, string>
128
+ }
129
+ if (itemWithSearch._search) {
130
+ for (const [parentKey, fields] of Object.entries(parentFields)) {
131
+ for (const field of fields) {
132
+ const key = `${parentKey}.${field}`
133
+ const value = itemWithSearch._search[key]
134
+ // _search values are always strings
135
+ if (value && value.toLowerCase().includes(searchLower)) {
136
+ return true
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return false
143
+ } else {
144
+ // No searchFields declared: search all string/number fields
145
+ return Object.values(item).some((value: unknown) => {
146
+ if (typeof value === 'string') {
147
+ return value.toLowerCase().includes(searchLower)
148
+ }
149
+ if (typeof value === 'number') {
150
+ return value.toString().includes(search)
151
+ }
152
+ return false
153
+ })
154
+ }
155
+ })
156
+ }
157
+
158
+ // Build query and apply filters using QueryExecutor
159
+ const queryObj = this._buildQuery(filters)
160
+ if (Object.keys(queryObj).length > 0) {
161
+ const { items: filtered } = QueryExecutor.execute(result, queryObj)
162
+ result = filtered as any[]
163
+ }
164
+
165
+ // Total after filtering (before pagination)
166
+ const total = result.length
167
+
168
+ // Apply sort (QueryExecutor does not sort)
169
+ if (sort_by) {
170
+ result.sort((a: any, b: any) => {
171
+ const aVal = a[sort_by]
172
+ const bVal = b[sort_by]
173
+
174
+ // Handle nulls
175
+ if (aVal == null && bVal == null) return 0
176
+ if (aVal == null) return sort_order === 'asc' ? 1 : -1
177
+ if (bVal == null) return sort_order === 'asc' ? -1 : 1
178
+
179
+ // Compare
180
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
181
+ const cmp = aVal.localeCompare(bVal)
182
+ return sort_order === 'asc' ? cmp : -cmp
183
+ }
184
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0
185
+ return sort_order === 'asc' ? cmp : -cmp
186
+ })
187
+ }
188
+
189
+ // Apply pagination
190
+ const start = (page - 1) * page_size
191
+ const paged = result.slice(start, start + page_size)
192
+
193
+ return { items: paged, total }
194
+ }
195
+
196
+ /**
197
+ * Get operation stats (for debug panel)
198
+ */
199
+ proto.getStats = function (this: Self): OperationStats {
200
+ return { ...this._stats }
201
+ }
202
+
203
+ /**
204
+ * Reset operation stats
205
+ */
206
+ proto.resetStats = function (this: Self): void {
207
+ this._stats = {
208
+ list: 0,
209
+ get: 0,
210
+ create: 0,
211
+ update: 0,
212
+ delete: 0,
213
+ cacheHits: 0,
214
+ cacheMisses: 0,
215
+ detailCacheHits: 0,
216
+ detailCacheMisses: 0,
217
+ maxItemsSeen: 0,
218
+ maxTotal: 0,
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,151 @@
1
+ import type { EntityRecord, ListParams, ListResult, ChildConfig, ParentConfig } from '../types'
2
+ import type { EntityManager } from './EntityManager'
3
+ import type { EntityManagerInternal } from './EntityManager.types'
4
+
5
+ type Self = EntityManagerInternal<any>
6
+
7
+ /**
8
+ * Patch EntityManager prototype with relation-related methods.
9
+ */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export function applyRelationsMethods(EntityManagerClass: { prototype: any }): void {
12
+ const proto = EntityManagerClass.prototype as Self
13
+
14
+ proto.getChildConfig = function (this: Self, childName: string): ChildConfig | undefined {
15
+ return this._children[childName]
16
+ }
17
+
18
+ proto.getChildNames = function (this: Self): string[] {
19
+ return Object.keys(this._children)
20
+ }
21
+
22
+ proto.getParentConfig = function (this: Self): ParentConfig | null {
23
+ return this._parent
24
+ }
25
+
26
+ proto.listChildren = async function (
27
+ this: Self,
28
+ parentId: string | number,
29
+ childName: string,
30
+ params: ListParams = {}
31
+ ): Promise<ListResult<EntityRecord>> {
32
+ const childConfig = this._children[childName]
33
+ if (!childConfig) {
34
+ throw new Error(
35
+ `[EntityManager:${this.name}] Unknown child relation "${childName}"`
36
+ )
37
+ }
38
+
39
+ // Build endpoint path
40
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
41
+
42
+ if (this.storage?.request) {
43
+ return this.storage.request('GET', childEndpoint, {
44
+ params,
45
+ }) as Promise<ListResult<EntityRecord>>
46
+ }
47
+
48
+ throw new Error(
49
+ `[EntityManager:${this.name}] listChildren() requires storage with request()`
50
+ )
51
+ }
52
+
53
+ proto.getChild = async function (
54
+ this: Self,
55
+ parentId: string | number,
56
+ childName: string,
57
+ childId: string | number
58
+ ): Promise<EntityRecord> {
59
+ const childConfig = this._children[childName]
60
+ if (!childConfig) {
61
+ throw new Error(
62
+ `[EntityManager:${this.name}] Unknown child relation "${childName}"`
63
+ )
64
+ }
65
+
66
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
67
+
68
+ if (this.storage?.request) {
69
+ return this.storage.request(
70
+ 'GET',
71
+ `${childEndpoint}/${childId}`
72
+ ) as Promise<EntityRecord>
73
+ }
74
+
75
+ throw new Error(
76
+ `[EntityManager:${this.name}] getChild() requires storage with request()`
77
+ )
78
+ }
79
+
80
+ proto.createChild = async function (
81
+ this: Self,
82
+ parentId: string | number,
83
+ childName: string,
84
+ data: Record<string, unknown>
85
+ ): Promise<EntityRecord> {
86
+ const childConfig = this._children[childName]
87
+ if (!childConfig) {
88
+ throw new Error(
89
+ `[EntityManager:${this.name}] Unknown child relation "${childName}"`
90
+ )
91
+ }
92
+
93
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
94
+
95
+ if (this.storage?.request) {
96
+ return this.storage.request('POST', childEndpoint, {
97
+ data,
98
+ }) as Promise<EntityRecord>
99
+ }
100
+
101
+ throw new Error(
102
+ `[EntityManager:${this.name}] createChild() requires storage with request()`
103
+ )
104
+ }
105
+
106
+ proto.deleteChild = async function (
107
+ this: Self,
108
+ parentId: string | number,
109
+ childName: string,
110
+ childId: string | number
111
+ ): Promise<void> {
112
+ const childConfig = this._children[childName]
113
+ if (!childConfig) {
114
+ throw new Error(
115
+ `[EntityManager:${this.name}] Unknown child relation "${childName}"`
116
+ )
117
+ }
118
+
119
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
120
+
121
+ if (this.storage?.request) {
122
+ await this.storage.request('DELETE', `${childEndpoint}/${childId}`)
123
+ return
124
+ }
125
+
126
+ throw new Error(
127
+ `[EntityManager:${this.name}] deleteChild() requires storage with request()`
128
+ )
129
+ }
130
+
131
+ proto.getParentManager = function (this: Self): EntityManager<EntityRecord> | null {
132
+ const parent = this._parent
133
+ const orchestrator = this._orchestrator
134
+ if (!parent || !orchestrator) return null
135
+ return (
136
+ orchestrator.get<EntityRecord>(parent.entity) ?? null
137
+ )
138
+ }
139
+
140
+ proto.getChildManager = function (
141
+ this: Self,
142
+ childName: string
143
+ ): EntityManager<EntityRecord> | null {
144
+ const childConfig = this._children[childName]
145
+ const orchestrator = this._orchestrator
146
+ if (!childConfig || !orchestrator) return null
147
+ return (
148
+ orchestrator.get<EntityRecord>(childConfig.entity) ?? null
149
+ )
150
+ }
151
+ }