@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,288 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EntityRecord,
|
|
3
|
+
ListResult,
|
|
4
|
+
IStorage,
|
|
5
|
+
FieldConfig,
|
|
6
|
+
ChildConfig,
|
|
7
|
+
ParentConfig,
|
|
8
|
+
RelationConfig,
|
|
9
|
+
NavConfig,
|
|
10
|
+
StorageCapabilities,
|
|
11
|
+
} from '../types'
|
|
12
|
+
import type { AuthUser, EntityAuthAdapter as IEntityAuthAdapter } from './auth/EntityAuthAdapter'
|
|
13
|
+
import type { SignalBus } from '../kernel/SignalBus'
|
|
14
|
+
import type { HookRegistry } from '../hooks/HookRegistry'
|
|
15
|
+
|
|
16
|
+
// Circular type import — safe because `import type` is erased at runtime
|
|
17
|
+
import type { EntityManager } from './EntityManager'
|
|
18
|
+
|
|
19
|
+
// ============ INTERNAL TYPES ============
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cache state for local filtering
|
|
23
|
+
*/
|
|
24
|
+
export interface CacheState<T> {
|
|
25
|
+
items: T[]
|
|
26
|
+
total: number
|
|
27
|
+
loadedAt: number | null
|
|
28
|
+
valid: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detail cache entry for asymmetric entities (full detail per item)
|
|
33
|
+
*/
|
|
34
|
+
export interface DetailCacheEntry<T> {
|
|
35
|
+
item: T
|
|
36
|
+
loadedAt: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detail cache state for asymmetric entities
|
|
41
|
+
*/
|
|
42
|
+
export interface DetailCacheState<T> {
|
|
43
|
+
items: Map<string, DetailCacheEntry<T>>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Operation stats for debugging
|
|
48
|
+
*/
|
|
49
|
+
export interface OperationStats {
|
|
50
|
+
list: number
|
|
51
|
+
get: number
|
|
52
|
+
create: number
|
|
53
|
+
update: number
|
|
54
|
+
delete: number
|
|
55
|
+
cacheHits: number
|
|
56
|
+
cacheMisses: number
|
|
57
|
+
detailCacheHits: number
|
|
58
|
+
detailCacheMisses: number
|
|
59
|
+
maxItemsSeen: number
|
|
60
|
+
maxTotal: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolved storage result
|
|
65
|
+
*/
|
|
66
|
+
export interface ResolvedStorage<T extends EntityRecord> {
|
|
67
|
+
storage: IStorage<T> | null | undefined
|
|
68
|
+
endpoint?: string
|
|
69
|
+
params?: Record<string, unknown>
|
|
70
|
+
isDynamic?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Storage resolution result (what resolveStorage returns)
|
|
75
|
+
*/
|
|
76
|
+
export type StorageResolution<T extends EntityRecord> =
|
|
77
|
+
| string
|
|
78
|
+
| ((context: RoutingContext | undefined) => string)
|
|
79
|
+
| {
|
|
80
|
+
storage?: IStorage<T> | null
|
|
81
|
+
endpoint?: string | ((context: RoutingContext | undefined) => string)
|
|
82
|
+
params?: Record<string, unknown>
|
|
83
|
+
}
|
|
84
|
+
| null
|
|
85
|
+
| undefined
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Routing context for multi-storage patterns
|
|
89
|
+
*/
|
|
90
|
+
export interface RoutingContext {
|
|
91
|
+
parentChain?: Array<{ entity: string; id: string | number }>
|
|
92
|
+
[key: string]: unknown
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Presave hook context
|
|
97
|
+
*/
|
|
98
|
+
export interface PresaveContext<T extends EntityRecord = EntityRecord> {
|
|
99
|
+
entity: string
|
|
100
|
+
record: Partial<T>
|
|
101
|
+
isNew: boolean
|
|
102
|
+
id?: string | number
|
|
103
|
+
manager: EntityManager<T>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Postsave hook context
|
|
108
|
+
*/
|
|
109
|
+
export interface PostsaveContext<T extends EntityRecord = EntityRecord> {
|
|
110
|
+
entity: string
|
|
111
|
+
record: Partial<T>
|
|
112
|
+
result: T
|
|
113
|
+
isNew: boolean
|
|
114
|
+
id?: string | number
|
|
115
|
+
manager: EntityManager<T>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Predelete hook context
|
|
120
|
+
*/
|
|
121
|
+
export interface PredeleteContext<T extends EntityRecord = EntityRecord> {
|
|
122
|
+
entity: string
|
|
123
|
+
id: string | number
|
|
124
|
+
manager: EntityManager<T>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Query options
|
|
129
|
+
*/
|
|
130
|
+
export interface QueryOptions {
|
|
131
|
+
context?: WhitelistContext
|
|
132
|
+
routingContext?: RoutingContext | null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whitelist check context
|
|
137
|
+
*/
|
|
138
|
+
export interface WhitelistContext {
|
|
139
|
+
module?: string
|
|
140
|
+
scope?: string
|
|
141
|
+
form?: string
|
|
142
|
+
field?: string
|
|
143
|
+
bypassPermissions?: boolean
|
|
144
|
+
reason?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Cache info for debugging
|
|
149
|
+
*/
|
|
150
|
+
export interface CacheInfo {
|
|
151
|
+
enabled: boolean
|
|
152
|
+
storageSupportsTotal: boolean
|
|
153
|
+
threshold: number
|
|
154
|
+
valid: boolean
|
|
155
|
+
overflow: boolean
|
|
156
|
+
itemCount: number
|
|
157
|
+
total: number
|
|
158
|
+
loadedAt: number | null
|
|
159
|
+
/** Effective TTL in milliseconds (0 = no TTL) */
|
|
160
|
+
ttlMs: number
|
|
161
|
+
/** Timestamp when cache expires (null if no TTL) */
|
|
162
|
+
expiresAt: number | null
|
|
163
|
+
/** Whether the cache has expired based on TTL */
|
|
164
|
+
expired: boolean
|
|
165
|
+
/** Whether the entity uses asymmetric mode (list vs detail separation) */
|
|
166
|
+
asymmetric: boolean
|
|
167
|
+
/** Detail cache info (only populated when asymmetric + detail cache enabled) */
|
|
168
|
+
detailCache: {
|
|
169
|
+
enabled: boolean
|
|
170
|
+
ttlMs: number
|
|
171
|
+
size: number
|
|
172
|
+
maxSize: number
|
|
173
|
+
} | null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Badge descriptor for entity item headers
|
|
178
|
+
*/
|
|
179
|
+
export interface EntityBadge {
|
|
180
|
+
label: string
|
|
181
|
+
severity?: 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Rich severity descriptor for a field value.
|
|
186
|
+
* Extends beyond a simple severity string to support icons and label overrides.
|
|
187
|
+
*/
|
|
188
|
+
export interface SeverityDescriptor {
|
|
189
|
+
severity: string
|
|
190
|
+
icon?: string
|
|
191
|
+
label?: string
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A severity map value: plain string (backward compat) or rich descriptor */
|
|
195
|
+
export type SeverityMapValue = string | SeverityDescriptor
|
|
196
|
+
|
|
197
|
+
/** Severity map: field value → severity string or descriptor */
|
|
198
|
+
export type SeverityMap = Record<string, SeverityMapValue>
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* EntityManager constructor options
|
|
202
|
+
*/
|
|
203
|
+
export interface EntityManagerOptions<T extends EntityRecord = EntityRecord> {
|
|
204
|
+
name: string
|
|
205
|
+
storage?: IStorage<T> | null
|
|
206
|
+
idField?: string
|
|
207
|
+
label?: string
|
|
208
|
+
labelPlural?: string
|
|
209
|
+
routePrefix?: string
|
|
210
|
+
labelField?: string | ((entity: T) => string)
|
|
211
|
+
/** Callback to compute badges for an entity item header */
|
|
212
|
+
badges?: (entity: T) => EntityBadge[]
|
|
213
|
+
fields?: Record<string, FieldConfig>
|
|
214
|
+
localFilterThreshold?: number | null
|
|
215
|
+
/** Cache TTL in milliseconds (0=disabled, -1=infinite, >0=TTL). Overrides global, overridden by storage. */
|
|
216
|
+
cacheTtlMs?: number | null
|
|
217
|
+
/** Asymmetric mode: list() and get() return different structures. get() skips list cache. */
|
|
218
|
+
asymmetric?: boolean
|
|
219
|
+
/** Detail cache TTL in milliseconds (0=disabled (default), -1=infinite, >0=TTL). Only used when asymmetric=true. */
|
|
220
|
+
detailCacheTtlMs?: number
|
|
221
|
+
/** Detail cache max size (0=unlimited (default), >0=max items). Oldest entries are evicted when exceeded. Only used when asymmetric=true. */
|
|
222
|
+
detailCacheMaxSize?: number
|
|
223
|
+
readOnly?: boolean
|
|
224
|
+
warmup?: boolean
|
|
225
|
+
authSensitive?: boolean
|
|
226
|
+
system?: boolean
|
|
227
|
+
scopeWhitelist?: string[] | null
|
|
228
|
+
isOwn?: ((record: T, user: AuthUser) => boolean) | null
|
|
229
|
+
children?: Record<string, ChildConfig>
|
|
230
|
+
parent?: ParentConfig | null
|
|
231
|
+
parents?: Record<string, ParentConfig>
|
|
232
|
+
relations?: Record<string, RelationConfig>
|
|
233
|
+
authAdapter?: IEntityAuthAdapter | null
|
|
234
|
+
nav?: NavConfig
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Forward declaration for Orchestrator (to avoid circular deps)
|
|
238
|
+
export interface Orchestrator {
|
|
239
|
+
get<T extends EntityRecord>(name: string): EntityManager<T> | undefined
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
241
|
+
kernel?: any
|
|
242
|
+
deferred?: DeferredRegistry | null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Forward declaration for DeferredRegistry
|
|
246
|
+
export interface DeferredRegistry {
|
|
247
|
+
has(key: string): boolean
|
|
248
|
+
await(key: string): Promise<unknown>
|
|
249
|
+
queue<T>(key: string, fn: () => Promise<T>): Promise<T>
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Exposes protected members of EntityManager for use by prototype-patched helper methods.
|
|
254
|
+
* @internal
|
|
255
|
+
*/
|
|
256
|
+
export type EntityManagerInternal<T extends EntityRecord = EntityRecord> = EntityManager<T> & {
|
|
257
|
+
// Protected fields accessed by helpers
|
|
258
|
+
_cache: CacheState<T>
|
|
259
|
+
_cacheLoading: Promise<boolean> | null
|
|
260
|
+
_cacheTtlMs: number | null
|
|
261
|
+
_asymmetric: boolean
|
|
262
|
+
_detailCache: DetailCacheState<T>
|
|
263
|
+
_detailCacheTtlMs: number
|
|
264
|
+
_detailCacheMaxSize: number
|
|
265
|
+
_detailInflight: Map<string, Promise<T>>
|
|
266
|
+
_orchestrator: Orchestrator | null
|
|
267
|
+
_parents: Record<string, ParentConfig>
|
|
268
|
+
_parent: ParentConfig | null
|
|
269
|
+
_children: Record<string, ChildConfig>
|
|
270
|
+
_authSensitive: boolean
|
|
271
|
+
_signals: SignalBus | null
|
|
272
|
+
_signalCleanup: (() => void) | null
|
|
273
|
+
_warmup: boolean
|
|
274
|
+
_stats: OperationStats
|
|
275
|
+
|
|
276
|
+
// Protected methods that stay in the class body
|
|
277
|
+
_normalizeResolveResult(result: StorageResolution<T>, context?: RoutingContext): ResolvedStorage<T>
|
|
278
|
+
_buildPresaveContext(data: Partial<T>, isNew: boolean, id?: string | number | null): PresaveContext<T>
|
|
279
|
+
_buildPostsaveContext(data: Partial<T>, result: T, isNew: boolean, id?: string | number | null): PostsaveContext<T>
|
|
280
|
+
_buildPredeleteContext(id: string | number): PredeleteContext<T>
|
|
281
|
+
_emitSignal(action: 'created' | 'updated' | 'deleted', data: Record<string, unknown>): void
|
|
282
|
+
_emitDataInvalidate(action: 'created' | 'updated' | 'deleted', id: string | number | undefined): void
|
|
283
|
+
_invokeHook(hookName: string, context: Record<string, unknown>): Promise<void>
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Re-export types used by helpers
|
|
287
|
+
export type { EntityRecord, ListResult, IStorage, StorageCapabilities }
|
|
288
|
+
export type { SignalBus, HookRegistry }
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompositeAuthAdapter - Routes to sub-adapters based on entity patterns
|
|
3
|
+
*
|
|
4
|
+
* Enables multi-source authentication where different entities may use
|
|
5
|
+
* different auth backends (internal JWT, external API keys, OAuth, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Pattern matching:
|
|
8
|
+
* - Exact match: 'products' matches only 'products'
|
|
9
|
+
* - Prefix glob: 'external-*' matches 'external-products', 'external-orders'
|
|
10
|
+
* - Suffix glob: '*-readonly' matches 'products-readonly'
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EntityAuthAdapter } from './EntityAuthAdapter'
|
|
14
|
+
import { authFactory, type AuthFactoryContext } from './factory'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pattern match entry
|
|
18
|
+
*/
|
|
19
|
+
interface PatternEntry {
|
|
20
|
+
pattern: string
|
|
21
|
+
regex: RegExp
|
|
22
|
+
adapter: EntityAuthAdapter
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Composite adapter config
|
|
27
|
+
*/
|
|
28
|
+
export interface CompositeAuthConfig {
|
|
29
|
+
default: EntityAuthAdapter | string | Record<string, unknown>
|
|
30
|
+
mapping?: Record<string, EntityAuthAdapter | string | Record<string, unknown>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* CompositeAuthAdapter - Routes to sub-adapters based on entity patterns
|
|
35
|
+
*/
|
|
36
|
+
export class CompositeAuthAdapter extends EntityAuthAdapter {
|
|
37
|
+
protected _default: EntityAuthAdapter
|
|
38
|
+
protected _exactMatches: Map<string, EntityAuthAdapter>
|
|
39
|
+
protected _patterns: PatternEntry[]
|
|
40
|
+
|
|
41
|
+
constructor(config: CompositeAuthConfig, context: AuthFactoryContext = {}) {
|
|
42
|
+
super()
|
|
43
|
+
|
|
44
|
+
const { default: defaultConfig, mapping = {} } = config
|
|
45
|
+
|
|
46
|
+
if (!defaultConfig) {
|
|
47
|
+
throw new Error('[CompositeAuthAdapter] "default" adapter is required')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Resolve default adapter via factory
|
|
51
|
+
this._default = authFactory(defaultConfig, context)
|
|
52
|
+
|
|
53
|
+
// Resolve mapped adapters
|
|
54
|
+
this._exactMatches = new Map()
|
|
55
|
+
this._patterns = []
|
|
56
|
+
|
|
57
|
+
for (const [pattern, adapterConfig] of Object.entries(mapping)) {
|
|
58
|
+
const adapter = authFactory(adapterConfig, context)
|
|
59
|
+
|
|
60
|
+
if (pattern.includes('*')) {
|
|
61
|
+
// Glob pattern: convert to regex
|
|
62
|
+
const regexPattern = pattern
|
|
63
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
64
|
+
.replace(/\*/g, '.*') // * → .*
|
|
65
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
66
|
+
this._patterns.push({ pattern, regex, adapter })
|
|
67
|
+
} else {
|
|
68
|
+
// Exact match
|
|
69
|
+
this._exactMatches.set(pattern, adapter)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the adapter for a specific entity
|
|
76
|
+
*/
|
|
77
|
+
protected _getAdapter(entity: string): EntityAuthAdapter {
|
|
78
|
+
// 1. Exact match
|
|
79
|
+
const exactMatch = this._exactMatches.get(entity)
|
|
80
|
+
if (exactMatch) {
|
|
81
|
+
return exactMatch
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. Pattern match (first wins)
|
|
85
|
+
for (const { regex, adapter } of this._patterns) {
|
|
86
|
+
if (regex.test(entity)) {
|
|
87
|
+
return adapter
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Default
|
|
92
|
+
return this._default
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if user can perform action on entity type (scope check)
|
|
97
|
+
*/
|
|
98
|
+
canPerform(entity: string, action: string): boolean {
|
|
99
|
+
return this._getAdapter(entity).canPerform(entity, action)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if user can access specific record (silo check)
|
|
104
|
+
*/
|
|
105
|
+
canAccessRecord(entity: string, record: unknown): boolean {
|
|
106
|
+
return this._getAdapter(entity).canAccessRecord(entity, record)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get current user from the default adapter
|
|
111
|
+
*/
|
|
112
|
+
getCurrentUser() {
|
|
113
|
+
return this._default.getCurrentUser()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if user is granted an attribute (role or permission)
|
|
118
|
+
*/
|
|
119
|
+
isGranted(attribute: string, subject: unknown = null): boolean {
|
|
120
|
+
// For entity-specific permissions (entity:action), route to appropriate adapter
|
|
121
|
+
if (attribute.includes(':') && !attribute.startsWith('ROLE_')) {
|
|
122
|
+
const parts = attribute.split(':')
|
|
123
|
+
const entity = parts[0] || ''
|
|
124
|
+
const adapter = this._getAdapter(entity)
|
|
125
|
+
if ('isGranted' in adapter && typeof adapter.isGranted === 'function') {
|
|
126
|
+
return adapter.isGranted(attribute, subject)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Global permissions use default adapter
|
|
131
|
+
if ('isGranted' in this._default && typeof this._default.isGranted === 'function') {
|
|
132
|
+
return this._default.isGranted(attribute, subject)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fallback for adapters without isGranted
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the default adapter
|
|
141
|
+
*/
|
|
142
|
+
get defaultAdapter(): EntityAuthAdapter {
|
|
143
|
+
return this._default
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get adapter info for debugging
|
|
148
|
+
*/
|
|
149
|
+
getAdapterInfo(): {
|
|
150
|
+
default: string
|
|
151
|
+
exactMatches: Record<string, string>
|
|
152
|
+
patterns: Array<{ pattern: string; adapter: string }>
|
|
153
|
+
} {
|
|
154
|
+
const info: {
|
|
155
|
+
default: string
|
|
156
|
+
exactMatches: Record<string, string>
|
|
157
|
+
patterns: Array<{ pattern: string; adapter: string }>
|
|
158
|
+
} = {
|
|
159
|
+
default: this._getAdapterName(this._default),
|
|
160
|
+
exactMatches: {},
|
|
161
|
+
patterns: [],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const [entity, adapter] of this._exactMatches) {
|
|
165
|
+
info.exactMatches[entity] = this._getAdapterName(adapter)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const { pattern, adapter } of this._patterns) {
|
|
169
|
+
info.patterns.push({
|
|
170
|
+
pattern,
|
|
171
|
+
adapter: this._getAdapterName(adapter),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return info
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get adapter name for debugging
|
|
180
|
+
*/
|
|
181
|
+
protected _getAdapterName(adapter: EntityAuthAdapter): string {
|
|
182
|
+
return adapter.constructor?.name || 'Unknown'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Factory function to create CompositeAuthAdapter
|
|
188
|
+
*/
|
|
189
|
+
export function createCompositeAuthAdapter(
|
|
190
|
+
config: CompositeAuthConfig,
|
|
191
|
+
context: AuthFactoryContext = {}
|
|
192
|
+
): CompositeAuthAdapter {
|
|
193
|
+
return new CompositeAuthAdapter(config, context)
|
|
194
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { SecurityChecker } from './SecurityChecker'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User type for auth checks
|
|
5
|
+
*/
|
|
6
|
+
export interface AuthUser {
|
|
7
|
+
id?: string | number
|
|
8
|
+
role?: string
|
|
9
|
+
roles?: string[]
|
|
10
|
+
permissions?: string[]
|
|
11
|
+
[key: string]: unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* EntityAuthAdapter options
|
|
16
|
+
*/
|
|
17
|
+
export interface EntityAuthAdapterOptions {
|
|
18
|
+
getCurrentUser?: () => AuthUser | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* EntityAuthAdapter - Thin layer for entity-level permission checks
|
|
23
|
+
*
|
|
24
|
+
* Provides entity-level permission checking by delegating to SecurityChecker.
|
|
25
|
+
* All methods have sensible defaults - subclass only if you need custom behavior.
|
|
26
|
+
*/
|
|
27
|
+
export class EntityAuthAdapter {
|
|
28
|
+
protected _securityChecker: SecurityChecker | null = null
|
|
29
|
+
protected _getCurrentUserCallback: (() => AuthUser | null) | null = null
|
|
30
|
+
|
|
31
|
+
constructor(options: EntityAuthAdapterOptions = {}) {
|
|
32
|
+
if (options.getCurrentUser) {
|
|
33
|
+
this._getCurrentUserCallback = options.getCurrentUser
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set the SecurityChecker instance for isGranted() delegation
|
|
39
|
+
*/
|
|
40
|
+
setSecurityChecker(checker: SecurityChecker): void {
|
|
41
|
+
this._securityChecker = checker
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if current user is granted an attribute (Symfony-like contract)
|
|
46
|
+
*
|
|
47
|
+
* This is the unified permission check method. It delegates to SecurityChecker
|
|
48
|
+
* if one is configured, otherwise returns true (permissive fallback).
|
|
49
|
+
*/
|
|
50
|
+
isGranted(attribute: string, subject: unknown = null): boolean {
|
|
51
|
+
if (!this._securityChecker) return true // Permissive if not configured
|
|
52
|
+
return this._securityChecker.isGranted(attribute, subject)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if current user can assign a specific role
|
|
57
|
+
*/
|
|
58
|
+
canAssignRole(targetRole: string): boolean {
|
|
59
|
+
if (!this._securityChecker) return true
|
|
60
|
+
return this._securityChecker.canAssignRole(targetRole)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get all roles that current user can assign
|
|
65
|
+
*/
|
|
66
|
+
getAssignableRoles(): string[] {
|
|
67
|
+
if (!this._securityChecker) return []
|
|
68
|
+
return this._securityChecker.getAssignableRoles()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if the current user can perform an action on an entity type (scope check)
|
|
73
|
+
*
|
|
74
|
+
* Default: delegates to isGranted('entity:{entity}:{action}')
|
|
75
|
+
* Override for custom authentication requirements or business rules.
|
|
76
|
+
*/
|
|
77
|
+
canPerform(_entity: string, _action: string): boolean {
|
|
78
|
+
return this.isGranted(`entity:${_entity}:${_action}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if the current user can access a specific record (silo check)
|
|
83
|
+
*
|
|
84
|
+
* Default: delegates to isGranted('entity:{entity}:read', record)
|
|
85
|
+
* Note: Ownership checks are typically handled via EntityManager's isOwn callback.
|
|
86
|
+
*/
|
|
87
|
+
canAccessRecord(_entity: string, record: unknown): boolean {
|
|
88
|
+
return this.isGranted(`entity:${_entity}:read`, record)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the current authenticated user
|
|
93
|
+
*
|
|
94
|
+
* Returns user from:
|
|
95
|
+
* 1. _getCurrentUserCallback if provided in constructor
|
|
96
|
+
* 2. Subclass override if using class extension pattern
|
|
97
|
+
* 3. null if neither is configured
|
|
98
|
+
*/
|
|
99
|
+
getCurrentUser(): AuthUser | null {
|
|
100
|
+
if (this._getCurrentUserCallback) {
|
|
101
|
+
return this._getCurrentUserCallback()
|
|
102
|
+
}
|
|
103
|
+
// Subclasses can override this method
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Valid action types for scope checks
|
|
110
|
+
*/
|
|
111
|
+
export const AuthActions = Object.freeze({
|
|
112
|
+
READ: 'read',
|
|
113
|
+
CREATE: 'create',
|
|
114
|
+
UPDATE: 'update',
|
|
115
|
+
DELETE: 'delete',
|
|
116
|
+
LIST: 'list',
|
|
117
|
+
} as const)
|
|
118
|
+
|
|
119
|
+
export type AuthAction = (typeof AuthActions)[keyof typeof AuthActions]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissiveAuthAdapter - Default adapter that allows all operations
|
|
3
|
+
*
|
|
4
|
+
* This adapter is used when no custom EntityAuthAdapter is provided to EntityManager.
|
|
5
|
+
* It returns `true` for all permission checks, effectively disabling authorization.
|
|
6
|
+
*
|
|
7
|
+
* Use cases:
|
|
8
|
+
* - Development/prototyping without auth setup
|
|
9
|
+
* - Backward compatibility with existing apps that don't use auth
|
|
10
|
+
* - Internal admin tools with implicit trust
|
|
11
|
+
* - Testing environments
|
|
12
|
+
*/
|
|
13
|
+
import { EntityAuthAdapter, type AuthUser } from './EntityAuthAdapter'
|
|
14
|
+
|
|
15
|
+
export class PermissiveAuthAdapter extends EntityAuthAdapter {
|
|
16
|
+
/**
|
|
17
|
+
* Always allows any action on any entity type
|
|
18
|
+
*/
|
|
19
|
+
canPerform(_entity: string, _action: string): boolean {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Always allows access to any record
|
|
25
|
+
*/
|
|
26
|
+
canAccessRecord(_entity: string, _record: unknown): boolean {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns null (no authenticated user in permissive mode)
|
|
32
|
+
*/
|
|
33
|
+
getCurrentUser(): AuthUser | null {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Factory function to create a PermissiveAuthAdapter
|
|
40
|
+
*/
|
|
41
|
+
export function createPermissiveAdapter(): PermissiveAuthAdapter {
|
|
42
|
+
return new PermissiveAuthAdapter()
|
|
43
|
+
}
|