@quazardous/qdadm 1.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- package/src/zones/zones.ts +191 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import type { StorageCapabilities, ParentConfig } from '../types'
|
|
2
|
+
import type { CacheInfo, EntityManagerInternal } from './EntityManager.types'
|
|
3
|
+
|
|
4
|
+
type Self = EntityManagerInternal<any>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Patch EntityManager prototype with cache-related getters and methods.
|
|
8
|
+
*/
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export function applyCacheMethods(EntityManagerClass: { prototype: any }): void {
|
|
11
|
+
const proto = EntityManagerClass.prototype as Self
|
|
12
|
+
|
|
13
|
+
// ============ THRESHOLD & TTL (getters) ============
|
|
14
|
+
|
|
15
|
+
Object.defineProperty(proto, 'effectiveThreshold', {
|
|
16
|
+
get(this: Self): number {
|
|
17
|
+
return this.localFilterThreshold ?? 100
|
|
18
|
+
},
|
|
19
|
+
configurable: true,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
Object.defineProperty(proto, 'effectiveCacheTtlMs', {
|
|
23
|
+
get(this: Self): number {
|
|
24
|
+
// 1. Storage capabilities (can be set dynamically from API headers)
|
|
25
|
+
const storageCaps =
|
|
26
|
+
(this.storage as unknown as { capabilities?: StorageCapabilities })?.capabilities ||
|
|
27
|
+
(this.storage?.constructor as { capabilities?: StorageCapabilities })?.capabilities
|
|
28
|
+
if (storageCaps?.cacheTtlMs !== undefined) {
|
|
29
|
+
return storageCaps.cacheTtlMs
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Entity-level config
|
|
33
|
+
if (this._cacheTtlMs !== null) {
|
|
34
|
+
return this._cacheTtlMs
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Global default from kernel options
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const globalTtl = (this._orchestrator as any)?.kernelOptions?.defaultEntityCacheTtlMs
|
|
40
|
+
if (globalTtl !== undefined) {
|
|
41
|
+
return globalTtl
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 4. Default: infinite (no expiration)
|
|
45
|
+
return -1
|
|
46
|
+
},
|
|
47
|
+
configurable: true,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
Object.defineProperty(proto, 'isCacheTtlDisabled', {
|
|
51
|
+
get(this: Self): boolean {
|
|
52
|
+
return this.effectiveCacheTtlMs === 0
|
|
53
|
+
},
|
|
54
|
+
configurable: true,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
Object.defineProperty(proto, 'storageSupportsTotal', {
|
|
58
|
+
get(this: Self): boolean {
|
|
59
|
+
const caps = (this.storage?.constructor as { capabilities?: Partial<StorageCapabilities> })
|
|
60
|
+
?.capabilities
|
|
61
|
+
return caps?.supportsTotal ?? false
|
|
62
|
+
},
|
|
63
|
+
configurable: true,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
Object.defineProperty(proto, 'storageSearchFields', {
|
|
67
|
+
get(this: Self): string[] | undefined {
|
|
68
|
+
const caps = (this.storage?.constructor as { capabilities?: Partial<StorageCapabilities> })
|
|
69
|
+
?.capabilities
|
|
70
|
+
return caps?.searchFields
|
|
71
|
+
},
|
|
72
|
+
configurable: true,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
Object.defineProperty(proto, 'isCacheEnabled', {
|
|
76
|
+
get(this: Self): boolean {
|
|
77
|
+
if (this.effectiveThreshold <= 0) return false
|
|
78
|
+
if (this.isCacheTtlDisabled) return false // TTL=0 disables cache
|
|
79
|
+
// Check capabilities (instance getter or static)
|
|
80
|
+
const caps =
|
|
81
|
+
(this.storage as unknown as { capabilities?: StorageCapabilities })?.capabilities ||
|
|
82
|
+
(this.storage?.constructor as { capabilities?: StorageCapabilities })?.capabilities
|
|
83
|
+
if (caps?.supportsCaching === false) return false
|
|
84
|
+
if (!this.storageSupportsTotal) return false
|
|
85
|
+
return true
|
|
86
|
+
},
|
|
87
|
+
configurable: true,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
Object.defineProperty(proto, 'overflow', {
|
|
91
|
+
get(this: Self): boolean {
|
|
92
|
+
const cache = this._cache
|
|
93
|
+
return cache.valid && cache.total > cache.items.length
|
|
94
|
+
},
|
|
95
|
+
configurable: true,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
Object.defineProperty(proto, 'warmupEnabled', {
|
|
99
|
+
get(this: Self): boolean {
|
|
100
|
+
return this._warmup && this.isCacheEnabled
|
|
101
|
+
},
|
|
102
|
+
configurable: true,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ============ ASYMMETRIC MODE ============
|
|
106
|
+
|
|
107
|
+
Object.defineProperty(proto, 'isAsymmetric', {
|
|
108
|
+
get(this: Self): boolean {
|
|
109
|
+
if (this._asymmetric) return true
|
|
110
|
+
// Fallback to storage capabilities
|
|
111
|
+
const caps =
|
|
112
|
+
(this.storage as unknown as { capabilities?: StorageCapabilities })?.capabilities ||
|
|
113
|
+
(this.storage?.constructor as { capabilities?: StorageCapabilities })?.capabilities
|
|
114
|
+
return caps?.asymmetric ?? false
|
|
115
|
+
},
|
|
116
|
+
configurable: true,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
Object.defineProperty(proto, 'effectiveDetailCacheTtlMs', {
|
|
120
|
+
get(this: Self): number {
|
|
121
|
+
return this._detailCacheTtlMs
|
|
122
|
+
},
|
|
123
|
+
configurable: true,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
Object.defineProperty(proto, 'effectiveDetailCacheMaxSize', {
|
|
127
|
+
get(this: Self): number {
|
|
128
|
+
return this._detailCacheMaxSize
|
|
129
|
+
},
|
|
130
|
+
configurable: true,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
Object.defineProperty(proto, 'isDetailCacheEnabled', {
|
|
134
|
+
get(this: Self): boolean {
|
|
135
|
+
return this.isAsymmetric && this._detailCacheTtlMs !== 0
|
|
136
|
+
},
|
|
137
|
+
configurable: true,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// ============ INTERNAL METHODS ============
|
|
141
|
+
|
|
142
|
+
proto._isCacheExpired = function (this: Self): boolean {
|
|
143
|
+
const cache = this._cache
|
|
144
|
+
if (!cache.valid || !cache.loadedAt) return false
|
|
145
|
+
const ttl = this.effectiveCacheTtlMs
|
|
146
|
+
if (ttl <= 0) return false // 0 = disabled (handled elsewhere), -1 = infinite
|
|
147
|
+
return Date.now() - cache.loadedAt > ttl
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
proto._isDetailCacheEntryExpired = function (this: Self, loadedAt: number): boolean {
|
|
151
|
+
const ttl = this.effectiveDetailCacheTtlMs
|
|
152
|
+
if (ttl < 0) return false // -1 = infinite
|
|
153
|
+
if (ttl === 0) return true // 0 = disabled
|
|
154
|
+
return Date.now() - loadedAt > ttl
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
proto._evictDetailCache = function (this: Self): void {
|
|
158
|
+
const maxSize = this._detailCacheMaxSize
|
|
159
|
+
if (maxSize <= 0) return // 0 = unlimited
|
|
160
|
+
const items = this._detailCache.items
|
|
161
|
+
if (items.size <= maxSize) return
|
|
162
|
+
|
|
163
|
+
// Evict oldest entries by loadedAt
|
|
164
|
+
const entries = [...items.entries()].sort((a, b) => a[1].loadedAt - b[1].loadedAt)
|
|
165
|
+
const toRemove = entries.length - maxSize
|
|
166
|
+
for (let i = 0; i < toRemove; i++) {
|
|
167
|
+
items.delete(entries[i][0])
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
proto._getStorageRequiresAuth = function (this: Self): boolean {
|
|
172
|
+
// Check instance capabilities first (may have dynamic requiresAuth)
|
|
173
|
+
const instanceCaps = (this.storage as unknown as { capabilities?: Partial<StorageCapabilities> })
|
|
174
|
+
?.capabilities
|
|
175
|
+
if (instanceCaps?.requiresAuth !== undefined) {
|
|
176
|
+
return instanceCaps.requiresAuth
|
|
177
|
+
}
|
|
178
|
+
// Fallback to static capabilities
|
|
179
|
+
const caps = (this.storage?.constructor as { capabilities?: Partial<StorageCapabilities> })
|
|
180
|
+
?.capabilities
|
|
181
|
+
return caps?.requiresAuth ?? false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
proto._parseSearchFields = function (
|
|
185
|
+
this: Self,
|
|
186
|
+
overrideSearchFields: string[] | null = null
|
|
187
|
+
): { ownFields: string[]; parentFields: Record<string, string[]> } {
|
|
188
|
+
const search = overrideSearchFields ?? this.storageSearchFields ?? []
|
|
189
|
+
const ownFields: string[] = []
|
|
190
|
+
const parentFields: Record<string, string[]> = {}
|
|
191
|
+
|
|
192
|
+
const parents: Record<string, ParentConfig> = this._parents
|
|
193
|
+
|
|
194
|
+
for (const field of search) {
|
|
195
|
+
if (!field.includes('.')) {
|
|
196
|
+
ownFields.push(field)
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Split on first dot only
|
|
201
|
+
const parts = field.split('.', 2)
|
|
202
|
+
const parentKey = parts[0]
|
|
203
|
+
const fieldName = parts[1]
|
|
204
|
+
|
|
205
|
+
if (!parentKey || !fieldName) {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Validate parentKey exists in parents config
|
|
210
|
+
if (!parents[parentKey]) {
|
|
211
|
+
console.warn(
|
|
212
|
+
`[EntityManager:${this.name}] Unknown parent '${parentKey}' in searchFields '${field}', skipping`
|
|
213
|
+
)
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Group by parentKey
|
|
218
|
+
if (!parentFields[parentKey]) {
|
|
219
|
+
parentFields[parentKey] = []
|
|
220
|
+
}
|
|
221
|
+
parentFields[parentKey].push(fieldName)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { ownFields, parentFields }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
proto._resolveSearchFields = async function (this: Self, items: unknown[]): Promise<void> {
|
|
228
|
+
const { parentFields } = this._parseSearchFields()
|
|
229
|
+
|
|
230
|
+
// No parent fields to resolve
|
|
231
|
+
if (Object.keys(parentFields).length === 0) return
|
|
232
|
+
|
|
233
|
+
const orchestrator = this._orchestrator
|
|
234
|
+
const parents: Record<string, ParentConfig> = this._parents
|
|
235
|
+
|
|
236
|
+
// Need orchestrator to access other managers
|
|
237
|
+
if (!orchestrator) {
|
|
238
|
+
console.warn(
|
|
239
|
+
`[EntityManager:${this.name}] No orchestrator, cannot resolve parent fields`
|
|
240
|
+
)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Process each parent type
|
|
245
|
+
for (const [parentKey, fields] of Object.entries(parentFields)) {
|
|
246
|
+
const config = parents[parentKey]
|
|
247
|
+
|
|
248
|
+
if (!config) {
|
|
249
|
+
console.warn(
|
|
250
|
+
`[EntityManager:${this.name}] Missing parent config for '${parentKey}'`
|
|
251
|
+
)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Extract unique parent IDs (deduplicated)
|
|
256
|
+
const ids = [
|
|
257
|
+
...new Set(
|
|
258
|
+
items
|
|
259
|
+
.map((i) => (i as Record<string, unknown>)[config.foreignKey as string] as string | number | undefined)
|
|
260
|
+
.filter((id): id is string | number => id != null)
|
|
261
|
+
),
|
|
262
|
+
]
|
|
263
|
+
if (ids.length === 0) continue
|
|
264
|
+
|
|
265
|
+
// Batch fetch parent entities
|
|
266
|
+
const manager = orchestrator.get(config.entity)
|
|
267
|
+
if (!manager) {
|
|
268
|
+
console.warn(
|
|
269
|
+
`[EntityManager:${this.name}] Manager not found for '${config.entity}'`
|
|
270
|
+
)
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const parentItems = await manager.getMany(ids)
|
|
275
|
+
const parentMap = new Map(
|
|
276
|
+
parentItems.map((p) => [p[manager.idField as keyof typeof p], p])
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Cache resolved values in _search (non-enumerable)
|
|
280
|
+
for (const item of items) {
|
|
281
|
+
// Initialize _search as non-enumerable if needed
|
|
282
|
+
const itemWithSearch = item as { _search?: Record<string, string>; [k: string]: unknown }
|
|
283
|
+
if (!itemWithSearch._search) {
|
|
284
|
+
Object.defineProperty(item, '_search', {
|
|
285
|
+
value: {},
|
|
286
|
+
enumerable: false,
|
|
287
|
+
writable: true,
|
|
288
|
+
configurable: true,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const parent = parentMap.get(itemWithSearch[config.foreignKey as string])
|
|
293
|
+
for (const field of fields) {
|
|
294
|
+
itemWithSearch._search![`${parentKey}.${field}`] =
|
|
295
|
+
(parent?.[field as keyof typeof parent] as string) ?? ''
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============ SIGNAL LISTENERS ============
|
|
302
|
+
|
|
303
|
+
proto._setupCacheListeners = function (this: Self): void {
|
|
304
|
+
const signals = this._signals
|
|
305
|
+
if (!signals) return
|
|
306
|
+
|
|
307
|
+
// Clean up existing listeners if any
|
|
308
|
+
const existingCleanup = this._signalCleanup
|
|
309
|
+
if (existingCleanup) {
|
|
310
|
+
existingCleanup()
|
|
311
|
+
this._signalCleanup = null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const cleanups: Array<() => void> = []
|
|
315
|
+
const parents: Record<string, ParentConfig> = this._parents
|
|
316
|
+
|
|
317
|
+
// Listen for parent data changes (if parents defined)
|
|
318
|
+
if (parents && Object.keys(parents).length > 0) {
|
|
319
|
+
const parentEntities = Object.values(parents).map((p) => p.entity)
|
|
320
|
+
cleanups.push(
|
|
321
|
+
signals.on(
|
|
322
|
+
'entity:data-invalidate',
|
|
323
|
+
(event: { name: string; data: unknown }) => {
|
|
324
|
+
const { entity } = (event.data || {}) as { entity?: string }
|
|
325
|
+
if (entity && parentEntities.includes(entity)) {
|
|
326
|
+
this._clearSearchCache()
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Listen for own entity data changes to invalidate detail cache
|
|
334
|
+
if (this.isAsymmetric && this.isDetailCacheEnabled) {
|
|
335
|
+
cleanups.push(
|
|
336
|
+
signals.on(
|
|
337
|
+
'entity:data-invalidate',
|
|
338
|
+
(event: { name: string; data: unknown }) => {
|
|
339
|
+
const { entity } = (event.data || {}) as { entity?: string }
|
|
340
|
+
if (entity === this.name) {
|
|
341
|
+
this.invalidateDetailCache()
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Listen for datalayer invalidation
|
|
349
|
+
cleanups.push(
|
|
350
|
+
signals.on(
|
|
351
|
+
'entity:datalayer-invalidate',
|
|
352
|
+
(event: { name: string; data: unknown }) => {
|
|
353
|
+
const { entity, actuator } = (event.data || {}) as {
|
|
354
|
+
entity?: string
|
|
355
|
+
actuator?: string
|
|
356
|
+
}
|
|
357
|
+
// Auth-triggered: only react if authSensitive
|
|
358
|
+
if (actuator === 'auth' && !this._authSensitive) return
|
|
359
|
+
|
|
360
|
+
// Check entity match (global or targeted)
|
|
361
|
+
if (!entity || entity === '*' || entity === this.name) {
|
|
362
|
+
this.invalidateDataLayer()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
// Combined cleanup function
|
|
369
|
+
this._signalCleanup = () => {
|
|
370
|
+
cleanups.forEach((cleanup) => cleanup())
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
proto._clearSearchCache = function (this: Self): void {
|
|
375
|
+
const cache = this._cache
|
|
376
|
+
if (!cache.valid) return
|
|
377
|
+
|
|
378
|
+
for (const item of cache.items) {
|
|
379
|
+
const itemWithSearch = item as { _search?: Record<string, string> }
|
|
380
|
+
if (itemWithSearch._search) {
|
|
381
|
+
itemWithSearch._search = {}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Invalidate cache so next list() refetches
|
|
386
|
+
this.invalidateCache()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============ CACHE STATE ============
|
|
390
|
+
|
|
391
|
+
proto.invalidateDetailCache = function (this: Self): void {
|
|
392
|
+
this._detailCache.items.clear()
|
|
393
|
+
this._detailInflight.clear()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
proto.invalidateCache = function (this: Self): void {
|
|
397
|
+
const cache = this._cache
|
|
398
|
+
cache.valid = false
|
|
399
|
+
cache.items = []
|
|
400
|
+
cache.total = 0
|
|
401
|
+
cache.loadedAt = null
|
|
402
|
+
this._cacheLoading = null
|
|
403
|
+
// Also clear detail cache
|
|
404
|
+
this.invalidateDetailCache()
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
proto.invalidateDataLayer = function (this: Self): void {
|
|
408
|
+
this.invalidateCache()
|
|
409
|
+
|
|
410
|
+
// Reset storage if it supports it (e.g., clear auth tokens, cached responses)
|
|
411
|
+
if (typeof this.storage?.reset === 'function') {
|
|
412
|
+
this.storage.reset()
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ============ CACHE LOADING ============
|
|
417
|
+
|
|
418
|
+
proto.ensureCache = async function (this: Self): Promise<boolean> {
|
|
419
|
+
if (!this.isCacheEnabled) return false
|
|
420
|
+
if (this._cache.valid) return true
|
|
421
|
+
|
|
422
|
+
// Avoid concurrent cache loads
|
|
423
|
+
if (this._cacheLoading) {
|
|
424
|
+
await this._cacheLoading
|
|
425
|
+
return this._cache.valid
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this._cacheLoading = this._loadCache()
|
|
429
|
+
const result = await this._cacheLoading
|
|
430
|
+
this._cacheLoading = null
|
|
431
|
+
return result
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
proto._loadCacheInBackground = function (this: Self): void {
|
|
435
|
+
this._cacheLoading = this._loadCache()
|
|
436
|
+
this._cacheLoading
|
|
437
|
+
.catch((err: unknown) =>
|
|
438
|
+
console.error(
|
|
439
|
+
`[EntityManager:${this.name}] Background cache load failed:`,
|
|
440
|
+
err
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
.finally(() => {
|
|
444
|
+
this._cacheLoading = null
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
proto._loadCache = async function (this: Self): Promise<boolean> {
|
|
449
|
+
const threshold = this.effectiveThreshold
|
|
450
|
+
|
|
451
|
+
// First, check total count with minimal request
|
|
452
|
+
// Use _internal to skip stats counting
|
|
453
|
+
const probe = await this.list({ page_size: 1, _internal: true })
|
|
454
|
+
|
|
455
|
+
if (probe.total > threshold) {
|
|
456
|
+
// Too many items, don't cache
|
|
457
|
+
this._cache.valid = false
|
|
458
|
+
return false
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Load all items (internal operation, skip stats)
|
|
462
|
+
const result = await this.list({
|
|
463
|
+
page_size: probe.total || threshold,
|
|
464
|
+
_internal: true,
|
|
465
|
+
})
|
|
466
|
+
const cache = this._cache
|
|
467
|
+
cache.items = result.items || []
|
|
468
|
+
cache.total = result.total
|
|
469
|
+
cache.loadedAt = Date.now()
|
|
470
|
+
cache.valid = true
|
|
471
|
+
// Resolve parent fields for search (book.title, user.username, etc.)
|
|
472
|
+
await this._resolveSearchFields(cache.items)
|
|
473
|
+
return true
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ============ WARMUP ============
|
|
477
|
+
|
|
478
|
+
proto.warmup = async function (this: Self): Promise<boolean | null> {
|
|
479
|
+
// Skip if warmup disabled or caching not applicable
|
|
480
|
+
if (!this._warmup) return null
|
|
481
|
+
if (!this.isCacheEnabled) return null
|
|
482
|
+
|
|
483
|
+
const deferred = this._orchestrator?.deferred
|
|
484
|
+
|
|
485
|
+
// Wait for auth if app uses authentication (user context affects cache)
|
|
486
|
+
if (deferred?.has('auth:ready')) {
|
|
487
|
+
await deferred.await('auth:ready')
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const key = `entity:${this.name}:cache`
|
|
491
|
+
|
|
492
|
+
if (!deferred) {
|
|
493
|
+
// Fallback: direct cache load without DeferredRegistry
|
|
494
|
+
return this.ensureCache()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Register in DeferredRegistry for loose coupling
|
|
498
|
+
return deferred.queue(key, () => this.ensureCache())
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
proto.getCacheInfo = function (this: Self): CacheInfo {
|
|
502
|
+
const ttlMs = this.effectiveCacheTtlMs
|
|
503
|
+
const cache = this._cache
|
|
504
|
+
const asymmetric = this.isAsymmetric
|
|
505
|
+
return {
|
|
506
|
+
enabled: this.isCacheEnabled,
|
|
507
|
+
storageSupportsTotal: this.storageSupportsTotal,
|
|
508
|
+
threshold: this.effectiveThreshold,
|
|
509
|
+
valid: cache.valid,
|
|
510
|
+
overflow: this.overflow,
|
|
511
|
+
itemCount: cache.items.length,
|
|
512
|
+
total: cache.total,
|
|
513
|
+
loadedAt: cache.loadedAt,
|
|
514
|
+
ttlMs,
|
|
515
|
+
expiresAt: cache.loadedAt && ttlMs > 0
|
|
516
|
+
? cache.loadedAt + ttlMs
|
|
517
|
+
: null,
|
|
518
|
+
expired: this._isCacheExpired(),
|
|
519
|
+
asymmetric,
|
|
520
|
+
detailCache: asymmetric && this.isDetailCacheEnabled
|
|
521
|
+
? {
|
|
522
|
+
enabled: true,
|
|
523
|
+
ttlMs: this.effectiveDetailCacheTtlMs,
|
|
524
|
+
size: this._detailCache.items.size,
|
|
525
|
+
maxSize: this._detailCacheMaxSize,
|
|
526
|
+
}
|
|
527
|
+
: null,
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|