@quazardous/qdadm 2.2.0 → 2.2.2
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/package.json +1 -1
- package/src/components/layout/PageHeader.vue +3 -2
- package/src/components/layout/PageNav.vue +12 -6
- package/src/components/pages/LoginPage.vue +5 -11
- package/src/components/show/ShowPage.vue +6 -5
- package/src/composables/useBareForm.ts +9 -22
- package/src/composables/useChildPage.ts +6 -11
- package/src/composables/useEntityItemFormPage.types.ts +3 -10
- package/src/composables/useEntityItemPage.ts +3 -6
- package/src/composables/useEntityItemShowPage.ts +3 -12
- package/src/composables/useListPage.types.ts +3 -11
- package/src/composables/useNavContext.ts +7 -11
- package/src/composables/useNavigation.ts +5 -7
- package/src/composables/useUserImpersonator.ts +6 -16
- package/src/entity/EntityManager.cache.ts +4 -1
- package/src/entity/EntityManager.crud.ts +6 -0
- package/src/entity/EntityManager.interface.ts +72 -0
- package/src/entity/EntityManager.query.ts +6 -3
- package/src/entity/EntityManager.ts +1 -0
- package/src/entity/EntityManager.types.ts +3 -0
- package/src/generated/managers/testManager.ts +0 -46
- package/src/kernel/Kernel.routing.ts +12 -3
- package/src/module/moduleRegistry.ts +15 -0
- package/src/orchestrator/Orchestrator.ts +14 -0
- package/src/query/FilterQuery.ts +11 -8
- package/src/security/EntityRolesProvider.ts +11 -16
- package/src/types/index.ts +16 -0
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { computed, inject, type Ref } from 'vue'
|
|
22
22
|
import Tag from 'primevue/tag'
|
|
23
|
+
import type { ButtonSeverity } from '../../types'
|
|
23
24
|
|
|
24
25
|
interface TitleParts {
|
|
25
26
|
simple?: string
|
|
@@ -30,7 +31,7 @@ interface TitleParts {
|
|
|
30
31
|
|
|
31
32
|
interface BadgeConfig {
|
|
32
33
|
label: string
|
|
33
|
-
severity?:
|
|
34
|
+
severity?: ButtonSeverity
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
interface Props {
|
|
@@ -80,7 +81,7 @@ const hasDecoratedTitle = computed((): boolean => {
|
|
|
80
81
|
v-for="badge in badges"
|
|
81
82
|
:key="badge.label"
|
|
82
83
|
:value="badge.label"
|
|
83
|
-
:severity="
|
|
84
|
+
:severity="badge.severity || 'secondary'"
|
|
84
85
|
class="entity-badge"
|
|
85
86
|
/>
|
|
86
87
|
<template v-if="!effectiveTitleParts && !simpleTitle">{{ title }}</template>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { computed, watch, inject, type Ref } from 'vue'
|
|
19
19
|
import { useRoute, useRouter } from 'vue-router'
|
|
20
|
-
import { getSiblingRoutes, getChildRoutes, type ModuleRoute, type ParentConfig } from '../../module/moduleRegistry'
|
|
20
|
+
import { getSiblingRoutes, getChildRoutes, routeParamsSatisfied, type ModuleRoute, type ParentConfig } from '../../module/moduleRegistry'
|
|
21
21
|
import { useOrchestrator } from '../../orchestrator/useOrchestrator.js'
|
|
22
22
|
import type { EntityManager } from '../../entity/EntityManager'
|
|
23
23
|
import type { EntityRecord } from '../../types'
|
|
@@ -70,12 +70,15 @@ const siblingNavlinks = computed<NavLink[]>(() => {
|
|
|
70
70
|
const { entity: parentEntityName, param } = parentConfig.value
|
|
71
71
|
const siblings = getSiblingRoutes(parentEntityName, param)
|
|
72
72
|
|
|
73
|
-
// Filter out
|
|
73
|
+
// Filter out item-level routes (create, edit, show) — they're not
|
|
74
|
+
// navigation tabs — and any route whose required params aren't all
|
|
75
|
+
// available here (#1205: a child show route needs its own :id, which
|
|
76
|
+
// doesn't exist at list level; useLink would throw).
|
|
74
77
|
const navRoutes = siblings.filter((r: ModuleRoute) => {
|
|
75
78
|
const name = r.name || ''
|
|
76
79
|
const path = r.path || ''
|
|
77
|
-
|
|
78
|
-
return
|
|
80
|
+
if (name.match(/-(create|edit|new|show)$/) || path.match(/\/(create|edit|new)$/)) return false
|
|
81
|
+
return routeParamsSatisfied(path, route.params as Record<string, unknown>)
|
|
79
82
|
})
|
|
80
83
|
|
|
81
84
|
// Build navlinks with current route params
|
|
@@ -109,11 +112,14 @@ const childNavlinks = computed<NavLink[]>(() => {
|
|
|
109
112
|
const children = getChildRoutes(entityName)
|
|
110
113
|
if (children.length === 0) return []
|
|
111
114
|
|
|
112
|
-
// Filter out
|
|
115
|
+
// Filter out item-level routes (create, edit, show) and any route whose
|
|
116
|
+
// required params aren't satisfiable from the parent id alone (#1205).
|
|
113
117
|
const navRoutes = children.filter((r: ModuleRoute) => {
|
|
114
118
|
const name = r.name || ''
|
|
115
119
|
const path = r.path || ''
|
|
116
|
-
|
|
120
|
+
if (name.match(/-(create|edit|new|show)$/) || path.match(/\/(create|edit|new)$/)) return false
|
|
121
|
+
const parentParam = (r.meta?.parent as ParentConfig)?.param || currentManager?.idField || 'id'
|
|
122
|
+
return routeParamsSatisfied(path, { [parentParam]: entityId })
|
|
117
123
|
})
|
|
118
124
|
|
|
119
125
|
if (navRoutes.length === 0) return []
|
|
@@ -23,6 +23,7 @@ import Card from 'primevue/card'
|
|
|
23
23
|
import InputText from 'primevue/inputtext'
|
|
24
24
|
import Password from 'primevue/password'
|
|
25
25
|
import Button from 'primevue/button'
|
|
26
|
+
import type { OrchestratorLike } from '../../entity/EntityManager.interface'
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Auth adapter interface
|
|
@@ -46,15 +47,8 @@ interface LoginResult {
|
|
|
46
47
|
/**
|
|
47
48
|
* Orchestrator interface
|
|
48
49
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
success: (summary: string, detail?: string, emitter?: string) => void
|
|
52
|
-
error: (summary: string, detail?: string, emitter?: string) => void
|
|
53
|
-
}
|
|
54
|
-
signals: {
|
|
55
|
-
emit: (signal: string, payload: unknown) => void
|
|
56
|
-
}
|
|
57
|
-
}
|
|
50
|
+
// #1191 — shared structural view (was: local redeclaration)
|
|
51
|
+
type Orchestrator = OrchestratorLike
|
|
58
52
|
|
|
59
53
|
/**
|
|
60
54
|
* App config interface
|
|
@@ -184,7 +178,7 @@ async function handleLogin(): Promise<void> {
|
|
|
184
178
|
})
|
|
185
179
|
|
|
186
180
|
// Emit auth:login signal for debug bar and other listeners
|
|
187
|
-
orchestrator?.signals
|
|
181
|
+
orchestrator?.signals?.emit('auth:login', { user: result.user })
|
|
188
182
|
orchestrator?.toast.success(
|
|
189
183
|
'Welcome',
|
|
190
184
|
`Logged in as ${result.user?.username || result.user?.email || username.value}`,
|
|
@@ -202,7 +196,7 @@ async function handleLogin(): Promise<void> {
|
|
|
202
196
|
|| axiosError.message
|
|
203
197
|
|| 'Invalid credentials'
|
|
204
198
|
|
|
205
|
-
orchestrator?.signals
|
|
199
|
+
orchestrator?.signals?.emit('auth:login:error', {
|
|
206
200
|
username: username.value,
|
|
207
201
|
error: message,
|
|
208
202
|
status: axiosError.response?.status
|
|
@@ -44,6 +44,7 @@ import Button from 'primevue/button'
|
|
|
44
44
|
import Message from 'primevue/message'
|
|
45
45
|
import FieldGroups from '../item/FieldGroups.vue'
|
|
46
46
|
import ShowField from './ShowField.vue'
|
|
47
|
+
import type { ButtonSeverity } from '../../types'
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Page title parts for PageHeader
|
|
@@ -61,7 +62,7 @@ interface ResolvedAction {
|
|
|
61
62
|
name: string
|
|
62
63
|
label: string
|
|
63
64
|
icon?: string
|
|
64
|
-
severity?:
|
|
65
|
+
severity?: ButtonSeverity
|
|
65
66
|
isLoading: boolean
|
|
66
67
|
isDisabled: boolean
|
|
67
68
|
onClick: () => void | Promise<void>
|
|
@@ -110,7 +111,7 @@ const props = defineProps({
|
|
|
110
111
|
titleParts: { type: Object as PropType<PageTitleParts | null>, default: null },
|
|
111
112
|
|
|
112
113
|
// Custom badges from entity manager
|
|
113
|
-
badges: { type: Array as PropType<Array<{ label: string; severity?:
|
|
114
|
+
badges: { type: Array as PropType<Array<{ label: string; severity?: ButtonSeverity }>>, default: () => [] },
|
|
114
115
|
|
|
115
116
|
// Fields (for auto-rendering - optional, can use #fields slot instead)
|
|
116
117
|
fields: { type: Array as PropType<ResolvedFieldConfig[]>, default: () => [] },
|
|
@@ -200,7 +201,7 @@ const fetchErrorMessage = computed<string | null>(() => {
|
|
|
200
201
|
:key="action.name"
|
|
201
202
|
:label="action.label"
|
|
202
203
|
:icon="action.icon"
|
|
203
|
-
:severity="
|
|
204
|
+
:severity="action.severity"
|
|
204
205
|
:loading="action.isLoading"
|
|
205
206
|
:disabled="action.isDisabled"
|
|
206
207
|
@click="action.onClick"
|
|
@@ -283,7 +284,7 @@ const fetchErrorMessage = computed<string | null>(() => {
|
|
|
283
284
|
:key="action.name"
|
|
284
285
|
:label="action.label"
|
|
285
286
|
:icon="action.icon"
|
|
286
|
-
:severity="
|
|
287
|
+
:severity="action.severity"
|
|
287
288
|
:loading="action.isLoading"
|
|
288
289
|
:disabled="action.isDisabled"
|
|
289
290
|
@click="action.onClick"
|
|
@@ -344,7 +345,7 @@ const fetchErrorMessage = computed<string | null>(() => {
|
|
|
344
345
|
:key="action.name"
|
|
345
346
|
:label="action.label"
|
|
346
347
|
:icon="action.icon"
|
|
347
|
-
:severity="
|
|
348
|
+
:severity="action.severity"
|
|
348
349
|
:loading="action.isLoading"
|
|
349
350
|
:disabled="action.isDisabled"
|
|
350
351
|
@click="action.onClick"
|
|
@@ -35,28 +35,14 @@ import { useDirtyState } from './useDirtyState'
|
|
|
35
35
|
import { useUnsavedChangesGuard, type GuardDialogState } from './useUnsavedChangesGuard'
|
|
36
36
|
import { useBreadcrumb, type BreadcrumbDisplayItem } from './useBreadcrumb'
|
|
37
37
|
import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
|
|
38
|
+
import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
|
|
39
|
+
|
|
40
|
+
// #1191 — shared minimal structural views (was: local redeclarations)
|
|
41
|
+
type EntityManager = EntityManagerLike
|
|
42
|
+
type Orchestrator = OrchestratorLike
|
|
43
|
+
|
|
38
44
|
|
|
39
|
-
/**
|
|
40
|
-
* Entity manager interface (minimal for this composable)
|
|
41
|
-
*/
|
|
42
|
-
interface EntityManager {
|
|
43
|
-
label?: string
|
|
44
|
-
labelField?: string
|
|
45
|
-
getEntityLabel: (data: unknown) => string
|
|
46
|
-
}
|
|
47
45
|
|
|
48
|
-
/**
|
|
49
|
-
* Orchestrator interface
|
|
50
|
-
*/
|
|
51
|
-
interface Orchestrator {
|
|
52
|
-
get: (entityName: string) => EntityManager
|
|
53
|
-
toast: {
|
|
54
|
-
success: (summary: string, detail?: string, emitter?: unknown) => void
|
|
55
|
-
error: (summary: string, detail?: string, emitter?: unknown) => void
|
|
56
|
-
warn: (summary: string, detail?: string, emitter?: unknown) => void
|
|
57
|
-
info: (summary: string, detail?: string, emitter?: unknown) => void
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
46
|
|
|
61
47
|
/**
|
|
62
48
|
* Toast helper interface
|
|
@@ -171,7 +157,7 @@ export function useBareForm(options: UseBareFormOptions): UseBareFormReturn {
|
|
|
171
157
|
const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
|
|
172
158
|
if (orchestrator) {
|
|
173
159
|
try {
|
|
174
|
-
manager = orchestrator.get(entity)
|
|
160
|
+
manager = orchestrator.get(entity) ?? null
|
|
175
161
|
} catch {
|
|
176
162
|
// Manager not found, continue without it
|
|
177
163
|
}
|
|
@@ -237,7 +223,8 @@ export function useBareForm(options: UseBareFormOptions): UseBareFormReturn {
|
|
|
237
223
|
return manager.getEntityLabel(formData)
|
|
238
224
|
}
|
|
239
225
|
if (typeof effectiveLabelField === 'function') {
|
|
240
|
-
|
|
226
|
+
// labelField's param is contravariant-`never` in the Like — safe to call
|
|
227
|
+
return (effectiveLabelField as (data: unknown) => string)(formData)
|
|
241
228
|
}
|
|
242
229
|
return (formData[effectiveLabelField as string] as string) || null
|
|
243
230
|
}
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
import { computed, inject, type ComputedRef } from 'vue'
|
|
16
16
|
import { useRoute } from 'vue-router'
|
|
17
17
|
import { useStackHydrator, type StackHydratorReturn } from '../chain/useStackHydrator'
|
|
18
|
+
import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
|
|
19
|
+
|
|
20
|
+
// #1191 — shared minimal structural views (was: local redeclarations)
|
|
21
|
+
type EntityManager = EntityManagerLike
|
|
22
|
+
type Orchestrator = OrchestratorLike
|
|
23
|
+
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Parent config from route meta
|
|
@@ -28,21 +34,10 @@ interface ParentConfig {
|
|
|
28
34
|
/**
|
|
29
35
|
* Entity manager interface (subset used by child pages)
|
|
30
36
|
*/
|
|
31
|
-
interface EntityManager {
|
|
32
|
-
idField: string
|
|
33
|
-
label?: string
|
|
34
|
-
labelPlural?: string
|
|
35
|
-
routePrefix?: string
|
|
36
|
-
getEntityLabel: (data: unknown) => string
|
|
37
|
-
getEntityBadges?: (data: unknown) => Array<{ label: string; severity?: string }>
|
|
38
|
-
}
|
|
39
37
|
|
|
40
38
|
/**
|
|
41
39
|
* Orchestrator interface
|
|
42
40
|
*/
|
|
43
|
-
interface Orchestrator {
|
|
44
|
-
get: (entityName: string) => EntityManager | null
|
|
45
|
-
}
|
|
46
41
|
|
|
47
42
|
/**
|
|
48
43
|
* Return type for useChildPage
|
|
@@ -10,20 +10,13 @@ import type { BreadcrumbDisplayItem } from './useBreadcrumb'
|
|
|
10
10
|
import type { ParentConfig, EntityManager } from './useEntityItemPage'
|
|
11
11
|
import type { FieldGroup, GroupDefinition, GroupOptions, MoveFieldPosition } from './useFieldManager'
|
|
12
12
|
import type { StackHydratorReturn } from '../chain/useStackHydrator'
|
|
13
|
+
import type { OrchestratorLike } from '../entity/EntityManager.interface'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Orchestrator interface
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
toast?: {
|
|
20
|
-
success: (summary: string, detail?: string, emitter?: unknown) => void
|
|
21
|
-
error: (summary: string, detail?: string, emitter?: unknown) => void
|
|
22
|
-
warn: (summary: string, detail?: string, emitter?: unknown) => void
|
|
23
|
-
info: (summary: string, detail?: string, emitter?: unknown) => void
|
|
24
|
-
[key: string]: ((summary: string, detail?: string, emitter?: unknown) => void) | undefined
|
|
25
|
-
}
|
|
26
|
-
}
|
|
18
|
+
// #1191 — shared structural view, parameterized on the CRUD tier
|
|
19
|
+
export type Orchestrator = OrchestratorLike<EntityManager>
|
|
27
20
|
|
|
28
21
|
/**
|
|
29
22
|
* Field definition from EntityManager schema
|
|
@@ -16,6 +16,7 @@ import { ref, computed, onMounted, inject, provide, type Ref, type ComputedRef }
|
|
|
16
16
|
import { useRoute } from 'vue-router'
|
|
17
17
|
import { useStackHydrator, type StackHydratorReturn } from '../chain/useStackHydrator'
|
|
18
18
|
import type { EntityManagerCrud } from '../entity/EntityManager.interface'
|
|
19
|
+
import type { OrchestratorLike } from '../entity/EntityManager.interface'
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Parent configuration from route.meta.parent
|
|
@@ -61,12 +62,8 @@ export interface CreateContext {
|
|
|
61
62
|
*/
|
|
62
63
|
export type EntityManager = EntityManagerCrud
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*/
|
|
67
|
-
interface Orchestrator {
|
|
68
|
-
get: (entityName: string) => EntityManager
|
|
69
|
-
}
|
|
65
|
+
// #1191 — shared structural view, parameterized on the CRUD tier
|
|
66
|
+
type Orchestrator = OrchestratorLike<EntityManager>
|
|
70
67
|
|
|
71
68
|
/**
|
|
72
69
|
* Options for useEntityItemPage
|
|
@@ -54,19 +54,10 @@ import {
|
|
|
54
54
|
type ShowFieldDefinition,
|
|
55
55
|
type ShowResolvedFieldConfig,
|
|
56
56
|
} from './createShowFieldResolver'
|
|
57
|
+
import type { OrchestratorLike } from '../entity/EntityManager.interface'
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*/
|
|
61
|
-
interface Orchestrator {
|
|
62
|
-
get: (entityName: string) => EntityManager
|
|
63
|
-
toast?: {
|
|
64
|
-
success: (summary: string, detail?: string, emitter?: unknown) => void
|
|
65
|
-
error: (summary: string, detail?: string, emitter?: unknown) => void
|
|
66
|
-
warn: (summary: string, detail?: string, emitter?: unknown) => void
|
|
67
|
-
info: (summary: string, detail?: string, emitter?: unknown) => void
|
|
68
|
-
}
|
|
69
|
-
}
|
|
59
|
+
// #1191 — shared structural view (manager tier from useEntityItemPage)
|
|
60
|
+
type Orchestrator = OrchestratorLike<EntityManager>
|
|
70
61
|
|
|
71
62
|
/**
|
|
72
63
|
* Field definition from EntityManager schema.
|
|
@@ -7,7 +7,7 @@ import type { Ref, ComputedRef } from 'vue'
|
|
|
7
7
|
import type { Router } from 'vue-router'
|
|
8
8
|
import type { FilterQuery } from '../query/FilterQuery'
|
|
9
9
|
import type { ParentConfig, UseEntityItemPageReturn } from './useEntityItemPage.js'
|
|
10
|
-
import type { EntityManagerRead } from '../entity/EntityManager.interface'
|
|
10
|
+
import type { EntityManagerRead, OrchestratorLike } from '../entity/EntityManager.interface'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Entity manager interface for list pages (re-export for convenience)
|
|
@@ -27,16 +27,8 @@ export interface ListResponse<T = unknown> {
|
|
|
27
27
|
/**
|
|
28
28
|
* Orchestrator interface
|
|
29
29
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
toast: {
|
|
33
|
-
success: (summary: string, detail?: string, emitter?: unknown) => void
|
|
34
|
-
error: (summary: string, detail?: string, emitter?: unknown) => void
|
|
35
|
-
warn: (summary: string, detail?: string, emitter?: unknown) => void
|
|
36
|
-
info: (summary: string, detail?: string, emitter?: unknown) => void
|
|
37
|
-
[key: string]: ((summary: string, detail?: string, emitter?: unknown) => void) | undefined
|
|
38
|
-
}
|
|
39
|
-
}
|
|
30
|
+
// #1191 — shared structural view, parameterized on the Read tier
|
|
31
|
+
export type Orchestrator = OrchestratorLike<EntityManager>
|
|
40
32
|
|
|
41
33
|
/**
|
|
42
34
|
* Toast helper interface
|
|
@@ -21,6 +21,12 @@ import { useRoute, useRouter, type RouteParamsRawGeneric } from 'vue-router'
|
|
|
21
21
|
import { getSiblingRoutes } from '../module/moduleRegistry'
|
|
22
22
|
import { useSemanticBreadcrumb, type SemanticBreadcrumbItem } from './useSemanticBreadcrumb'
|
|
23
23
|
import { useStackHydrator, type HydratedLevel } from '../chain/useStackHydrator'
|
|
24
|
+
import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
|
|
25
|
+
|
|
26
|
+
// #1191 — shared minimal structural views (was: local redeclarations)
|
|
27
|
+
type EntityManager = EntityManagerLike
|
|
28
|
+
type Orchestrator = OrchestratorLike
|
|
29
|
+
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* Navigation chain segment types
|
|
@@ -44,20 +50,10 @@ export interface NavChainSegment {
|
|
|
44
50
|
/**
|
|
45
51
|
* Entity manager interface
|
|
46
52
|
*/
|
|
47
|
-
interface EntityManager {
|
|
48
|
-
routePrefix?: string
|
|
49
|
-
labelPlural?: string
|
|
50
|
-
idField?: string
|
|
51
|
-
readOnly?: boolean
|
|
52
|
-
getEntityLabel: (data: unknown) => string
|
|
53
|
-
}
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
55
|
* Orchestrator interface
|
|
57
56
|
*/
|
|
58
|
-
interface Orchestrator {
|
|
59
|
-
get: (entityName: string) => EntityManager | null
|
|
60
|
-
}
|
|
61
57
|
|
|
62
58
|
/**
|
|
63
59
|
* Breadcrumb item
|
|
@@ -307,7 +303,7 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
|
|
|
307
303
|
})
|
|
308
304
|
} else if (segment.type === 'item') {
|
|
309
305
|
const data = chainData.value.get(i)
|
|
310
|
-
const label = data && segment.manager ? segment.manager.getEntityLabel(data) : '...'
|
|
306
|
+
const label = (data && segment.manager ? segment.manager.getEntityLabel(data) : null) ?? '...'
|
|
311
307
|
const idField = segment.manager?.idField || 'id'
|
|
312
308
|
|
|
313
309
|
// Only create link if we have a valid id
|
|
@@ -15,6 +15,11 @@ import { getNavSections, alterMenuSections, isMenuAltered } from '../module/modu
|
|
|
15
15
|
import { useSemanticBreadcrumb } from './useSemanticBreadcrumb'
|
|
16
16
|
import { useI18n } from '../i18n/useI18n'
|
|
17
17
|
import type { HookRegistry } from '../hooks/HookRegistry'
|
|
18
|
+
import type { OrchestratorLike } from '../entity/EntityManager.interface'
|
|
19
|
+
|
|
20
|
+
// #1191 — shared minimal structural view (was: local redeclaration)
|
|
21
|
+
type Orchestrator = OrchestratorLike
|
|
22
|
+
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* Navigation item interface
|
|
@@ -39,17 +44,10 @@ export interface NavSection {
|
|
|
39
44
|
/**
|
|
40
45
|
* Entity manager interface
|
|
41
46
|
*/
|
|
42
|
-
interface EntityManager {
|
|
43
|
-
canRead: () => boolean
|
|
44
|
-
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Orchestrator interface
|
|
48
50
|
*/
|
|
49
|
-
interface Orchestrator {
|
|
50
|
-
signals?: unknown
|
|
51
|
-
get: (entityName: string) => EntityManager | null
|
|
52
|
-
}
|
|
53
51
|
|
|
54
52
|
/**
|
|
55
53
|
* Return type for useNavigation
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type Ref,
|
|
38
38
|
type ComputedRef,
|
|
39
39
|
} from 'vue'
|
|
40
|
-
import type {
|
|
40
|
+
import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* User record type
|
|
@@ -55,28 +55,18 @@ interface AuthAdapterWithImpersonation {
|
|
|
55
55
|
getOriginalUser?: () => UserRecord | null
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/**
|
|
59
|
-
* Entity auth adapter interface
|
|
60
|
-
*/
|
|
61
|
-
interface EntityAuthAdapter {
|
|
62
|
-
isGranted?: (permission: string) => boolean
|
|
63
|
-
}
|
|
64
|
-
|
|
65
58
|
/**
|
|
66
59
|
* Orchestrator interface
|
|
67
60
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
get: (entityName: string) => EntityManager | null
|
|
61
|
+
// #1191 — shared structural view + the impersonator's extra surface
|
|
62
|
+
type ImpersonatorManager = EntityManagerLike & {
|
|
63
|
+
findAll?: () => Promise<{ data?: UserRecord[] } | UserRecord[]>
|
|
72
64
|
}
|
|
65
|
+
type Orchestrator = OrchestratorLike<ImpersonatorManager | null | undefined>
|
|
73
66
|
|
|
74
67
|
/**
|
|
75
68
|
* Entity manager interface
|
|
76
69
|
*/
|
|
77
|
-
interface EntityManager {
|
|
78
|
-
findAll: () => Promise<{ data?: UserRecord[] } | UserRecord[]>
|
|
79
|
-
}
|
|
80
70
|
|
|
81
71
|
/**
|
|
82
72
|
* Options for useUserImpersonator
|
|
@@ -214,7 +204,7 @@ export function useUserImpersonator(
|
|
|
214
204
|
error.value = null
|
|
215
205
|
const manager = orchestrator.get(entity)
|
|
216
206
|
if (manager) {
|
|
217
|
-
const result = await manager.findAll()
|
|
207
|
+
const result = await manager.findAll?.()
|
|
218
208
|
users.value =
|
|
219
209
|
(result as { data?: UserRecord[] })?.data ||
|
|
220
210
|
(result as UserRecord[]) ||
|
|
@@ -90,7 +90,9 @@ export function applyCacheMethods(EntityManagerClass: { prototype: any }): void
|
|
|
90
90
|
Object.defineProperty(proto, 'overflow', {
|
|
91
91
|
get(this: Self): boolean {
|
|
92
92
|
const cache = this._cache
|
|
93
|
-
|
|
93
|
+
// overflowed: total exceeded effectiveThreshold, cache was never
|
|
94
|
+
// filled (#1204) — the cache is unusable either way.
|
|
95
|
+
return cache.overflowed === true || (cache.valid && cache.total > cache.items.length)
|
|
94
96
|
},
|
|
95
97
|
configurable: true,
|
|
96
98
|
})
|
|
@@ -398,6 +400,7 @@ export function applyCacheMethods(EntityManagerClass: { prototype: any }): void
|
|
|
398
400
|
cache.valid = false
|
|
399
401
|
cache.items = []
|
|
400
402
|
cache.total = 0
|
|
403
|
+
cache.overflowed = false
|
|
401
404
|
cache.loadedAt = null
|
|
402
405
|
this._cacheLoading = null
|
|
403
406
|
// Also clear detail cache
|
|
@@ -182,6 +182,7 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
|
|
|
182
182
|
cache.items = items
|
|
183
183
|
cache.total = total
|
|
184
184
|
cache.valid = true
|
|
185
|
+
cache.overflowed = false
|
|
185
186
|
cache.loadedAt = Date.now()
|
|
186
187
|
// Resolve parent fields for search (book.title, user.username, etc.)
|
|
187
188
|
await this._resolveSearchFields(items)
|
|
@@ -191,6 +192,11 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
|
|
|
191
192
|
// Fire-and-forget: load full cache in background
|
|
192
193
|
this._loadCacheInBackground()
|
|
193
194
|
}
|
|
195
|
+
} else if (canUseCache && this.isCacheEnabled && total > this.effectiveThreshold) {
|
|
196
|
+
// Entity outgrew the threshold: the cache can never hold it. Mark it
|
|
197
|
+
// so query() stops filtering an empty cache and goes to the API
|
|
198
|
+
// instead of returning an empty list (#1204).
|
|
199
|
+
cache.overflowed = true
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
return { items, total, fromCache: false }
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* that flows through to method signatures for typed entity data.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { EntityBadge } from './EntityManager.types'
|
|
19
|
+
import type { SignalBus } from '../kernel/SignalBus'
|
|
20
|
+
import type { EntityAuthAdapter } from './auth/EntityAuthAdapter'
|
|
21
|
+
|
|
18
22
|
// ─── Base ────────────────────────────────────────────────────────────────────
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -76,3 +80,71 @@ export interface EntityManagerCrud<T = unknown> extends EntityManagerRead<T> {
|
|
|
76
80
|
getSeverity?: (field: string, value: string | number, defaultSeverity?: string) => string
|
|
77
81
|
getSeverityDescriptor?: (field: string, value: string | number, defaultSeverity?: string) => { severity: string; icon?: string; label?: string }
|
|
78
82
|
}
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// ASSIGNMENT-SAFE STRUCTURAL VIEWS (#1191)
|
|
85
|
+
// ============================================================================
|
|
86
|
+
//
|
|
87
|
+
// Composables and components used to redeclare tiny local `EntityManager` /
|
|
88
|
+
// `Orchestrator` interfaces (24 sites) because the canonical generics don't
|
|
89
|
+
// assign across erased type parameters. These shared, non-generic views are
|
|
90
|
+
// the single replacement: the canonical classes satisfy them structurally
|
|
91
|
+
// (compile-time asserted in orchestrator/Orchestrator.ts), and consumers
|
|
92
|
+
// declare exactly the surface they touch by intersecting when they need more.
|
|
93
|
+
//
|
|
94
|
+
// NOTE: members use METHOD syntax on purpose — TS keeps method parameters
|
|
95
|
+
// bivariant (even under strictFunctionTypes), which is what lets
|
|
96
|
+
// `EntityManager<T>` assign to the erased signatures below.
|
|
97
|
+
|
|
98
|
+
/** Toast surface exposed by the orchestrator (structural ToastHelper). */
|
|
99
|
+
export interface ToastLike {
|
|
100
|
+
success(summary: string, detail?: string, emitter?: unknown): void
|
|
101
|
+
error(summary: string, detail?: string, emitter?: unknown): void
|
|
102
|
+
warn(summary: string, detail?: string, emitter?: unknown): void
|
|
103
|
+
info(summary: string, detail?: string, emitter?: unknown): void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Minimal structural view of an EntityManager for composables/components.
|
|
108
|
+
* Every member exists on the canonical `EntityManager`; optionals reflect
|
|
109
|
+
* genuinely optional configuration.
|
|
110
|
+
*/
|
|
111
|
+
export interface EntityManagerLike {
|
|
112
|
+
idField: string
|
|
113
|
+
label?: string
|
|
114
|
+
labelPlural?: string
|
|
115
|
+
// Function-typed PROPERTY → strictly contravariant param; `never` is the
|
|
116
|
+
// contravariant top, so the canonical `(entity: T) => string` assigns.
|
|
117
|
+
// Cast at the call site when invoking.
|
|
118
|
+
labelField?: string | ((data: never) => string)
|
|
119
|
+
routePrefix?: string
|
|
120
|
+
readOnly?: boolean
|
|
121
|
+
getEntityLabel(data: unknown): string | null
|
|
122
|
+
getEntityBadges?(data: unknown): EntityBadge[]
|
|
123
|
+
canRead(entity?: unknown): boolean
|
|
124
|
+
canCreate(): boolean
|
|
125
|
+
canUpdate(entity?: unknown): boolean
|
|
126
|
+
canDelete(entity?: unknown): boolean
|
|
127
|
+
list(params?: Record<string, unknown>, context?: unknown): Promise<{
|
|
128
|
+
items: unknown[]
|
|
129
|
+
total?: number
|
|
130
|
+
fromCache?: boolean
|
|
131
|
+
}>
|
|
132
|
+
get(id: string | number, context?: unknown): Promise<unknown>
|
|
133
|
+
create(data: Record<string, unknown>, context?: unknown): Promise<unknown>
|
|
134
|
+
update(id: string | number, data: Record<string, unknown>, context?: unknown): Promise<unknown>
|
|
135
|
+
delete(id: string | number, context?: unknown): Promise<unknown>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Minimal structural view of the Orchestrator for composables/components.
|
|
140
|
+
* `get()` is typed nullable so consumers guard — the canonical class throws
|
|
141
|
+
* on unknown names, injected test doubles commonly return null.
|
|
142
|
+
*/
|
|
143
|
+
export interface OrchestratorLike<M = EntityManagerLike | null | undefined> {
|
|
144
|
+
get(name: string): M
|
|
145
|
+
has?(name: string): boolean
|
|
146
|
+
isRegistered?(name: string): boolean
|
|
147
|
+
toast: ToastLike
|
|
148
|
+
signals?: SignalBus | null
|
|
149
|
+
entityAuthAdapter?: EntityAuthAdapter | null
|
|
150
|
+
}
|
|
@@ -31,7 +31,7 @@ export function applyQueryMethods(EntityManagerClass: { prototype: any }): void
|
|
|
31
31
|
// Ensure cache is filled (via list) — skip for dynamic endpoints
|
|
32
32
|
// since list() disables caching when resolveStorage provides an endpoint
|
|
33
33
|
const cache = this._cache
|
|
34
|
-
if (!isDynamicEndpoint && !cache.valid && this.isCacheEnabled) {
|
|
34
|
+
if (!isDynamicEndpoint && !cache.valid && !cache.overflowed && this.isCacheEnabled) {
|
|
35
35
|
await this.list(
|
|
36
36
|
{ page_size: this.effectiveThreshold },
|
|
37
37
|
routingContext ?? undefined
|
|
@@ -40,8 +40,11 @@ export function applyQueryMethods(EntityManagerClass: { prototype: any }): void
|
|
|
40
40
|
|
|
41
41
|
let result: ListResult<any>
|
|
42
42
|
|
|
43
|
-
// Dynamic endpoint, overflow,
|
|
44
|
-
|
|
43
|
+
// Dynamic endpoint, overflow, cache disabled, or cache still not
|
|
44
|
+
// valid (fill failed / threshold exceeded) → use API directly. The
|
|
45
|
+
// cache is an optional layer: it must never turn into an empty
|
|
46
|
+
// result when it can't hold the data (#1204).
|
|
47
|
+
if (isDynamicEndpoint || this.overflow || !this.isCacheEnabled || !cache.valid) {
|
|
45
48
|
result = await this.list(params, routingContext ?? undefined)
|
|
46
49
|
} else {
|
|
47
50
|
// Full cache available - filter locally
|
|
@@ -26,6 +26,8 @@ export interface CacheState<T> {
|
|
|
26
26
|
total: number
|
|
27
27
|
loadedAt: number | null
|
|
28
28
|
valid: boolean
|
|
29
|
+
/** Entity total exceeded effectiveThreshold — cache can't hold it (#1204). */
|
|
30
|
+
overflowed?: boolean
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -286,3 +288,4 @@ export type EntityManagerInternal<T extends EntityRecord = EntityRecord> = Entit
|
|
|
286
288
|
// Re-export types used by helpers
|
|
287
289
|
export type { EntityRecord, ListResult, IStorage, StorageCapabilities }
|
|
288
290
|
export type { SignalBus, HookRegistry }
|
|
291
|
+
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TestManager - Auto-generated EntityManager
|
|
3
|
-
*
|
|
4
|
-
* Generated by qdadm/gen/generateManagers
|
|
5
|
-
* DO NOT EDIT MANUALLY - Changes will be overwritten
|
|
6
|
-
*
|
|
7
|
-
* Entity: test
|
|
8
|
-
* Endpoint: /test
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { EntityManager } from '@quazardous/qdadm'
|
|
12
|
-
import type { EntityRecord } from '@quazardous/qdadm'
|
|
13
|
-
import { ApiStorage } from '@quazardous/qdadm'
|
|
14
|
-
|
|
15
|
-
export interface TestEntity extends EntityRecord {
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Schema definition for test
|
|
20
|
-
*/
|
|
21
|
-
export const testSchema = {
|
|
22
|
-
name: "test",
|
|
23
|
-
endpoint: "/test",
|
|
24
|
-
fields: {}
|
|
25
|
-
} as const
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Storage options for test
|
|
29
|
-
*/
|
|
30
|
-
const storageOptions = {
|
|
31
|
-
endpoint: "/test"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* TestManager instance
|
|
36
|
-
*
|
|
37
|
-
* Provides CRUD operations for test entity.
|
|
38
|
-
*/
|
|
39
|
-
export const testManager = new EntityManager<TestEntity>({
|
|
40
|
-
...{
|
|
41
|
-
name: "test",
|
|
42
|
-
idField: "id",
|
|
43
|
-
fields: {}
|
|
44
|
-
},
|
|
45
|
-
storage: new ApiStorage<TestEntity>(storageOptions)
|
|
46
|
-
})
|
|
@@ -212,7 +212,11 @@ export function applyRoutingMethods(KernelClass: { prototype: any }): void {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
const entity = to.meta?.entity as string | undefined
|
|
215
|
-
|
|
215
|
+
// Entity not registered → nothing to check yet (e.g. child route
|
|
216
|
+
// declared before its manager loads) — allow. This is the ONLY
|
|
217
|
+
// allowed pass-through; a failing access check must fail CLOSED,
|
|
218
|
+
// not silently allow (#1190).
|
|
219
|
+
if (entity && this.orchestrator && this.orchestrator.isRegistered(entity)) {
|
|
216
220
|
try {
|
|
217
221
|
const manager = this.orchestrator.get(entity)
|
|
218
222
|
if (manager && !manager.canRead()) {
|
|
@@ -232,8 +236,13 @@ export function applyRoutingMethods(KernelClass: { prototype: any }): void {
|
|
|
232
236
|
next({ path: '/' })
|
|
233
237
|
return
|
|
234
238
|
}
|
|
235
|
-
} catch {
|
|
236
|
-
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(
|
|
241
|
+
`[qdadm] Access check failed for ${to.path} (entity: ${entity}) — denying navigation:`,
|
|
242
|
+
err
|
|
243
|
+
)
|
|
244
|
+
next({ path: '/' })
|
|
245
|
+
return
|
|
237
246
|
}
|
|
238
247
|
}
|
|
239
248
|
|
|
@@ -442,6 +442,21 @@ export function getEntityConfig(name: string): EntityConfig | undefined {
|
|
|
442
442
|
* @param parentParam - Parent route param
|
|
443
443
|
* @returns Routes with matching parent config
|
|
444
444
|
*/
|
|
445
|
+
/**
|
|
446
|
+
* Check that every REQUIRED `:param` in a route path is present (non-empty)
|
|
447
|
+
* in the given params. Used by PageNav to avoid building navlinks that
|
|
448
|
+
* vue-router's useLink would reject with "Missing required param" (#1205).
|
|
449
|
+
*/
|
|
450
|
+
export function routeParamsSatisfied(path: string, params: Record<string, unknown>): boolean {
|
|
451
|
+
const tokens = path.match(/:([A-Za-z0-9_]+)\??/g) || []
|
|
452
|
+
return tokens.every((token) => {
|
|
453
|
+
if (token.endsWith('?')) return true // optional param
|
|
454
|
+
const name = token.slice(1)
|
|
455
|
+
const value = params[name]
|
|
456
|
+
return value !== undefined && value !== null && value !== ''
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
445
460
|
export function getSiblingRoutes(parentEntity: string, parentParam: string): ModuleRoute[] {
|
|
446
461
|
return routes.filter((route) => {
|
|
447
462
|
const parent = route.meta?.parent
|
|
@@ -34,6 +34,7 @@ import type { EntityRecord } from '../types'
|
|
|
34
34
|
import type { SignalBus } from '../kernel/SignalBus'
|
|
35
35
|
import type { HookRegistry } from '../hooks/HookRegistry'
|
|
36
36
|
import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
|
|
37
|
+
import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* DeferredRegistry interface (minimal for Orchestrator needs)
|
|
@@ -425,3 +426,16 @@ Example fix in your module:
|
|
|
425
426
|
export function createOrchestrator(options?: OrchestratorOptions): Orchestrator {
|
|
426
427
|
return new Orchestrator(options)
|
|
427
428
|
}
|
|
429
|
+
|
|
430
|
+
// ============================================================================
|
|
431
|
+
// #1191 — compile-time guarantee that the canonical classes satisfy the
|
|
432
|
+
// minimal structural views consumed across composables/components. If a
|
|
433
|
+
// rename breaks the structural match, this is where the compiler complains.
|
|
434
|
+
// ============================================================================
|
|
435
|
+
type _AssertManagerLike<T extends EntityManagerLike> = T
|
|
436
|
+
type _AssertOrchestratorLike<T extends OrchestratorLike> = T
|
|
437
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
438
|
+
type _LikeChecks = [
|
|
439
|
+
_AssertManagerLike<EntityManager>,
|
|
440
|
+
_AssertOrchestratorLike<Orchestrator>,
|
|
441
|
+
]
|
package/src/query/FilterQuery.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface FilterQueryOptions<T = unknown> {
|
|
|
52
52
|
source: FilterQuerySource
|
|
53
53
|
entity?: string | null
|
|
54
54
|
field?: string | null
|
|
55
|
-
parentManager?:
|
|
55
|
+
parentManager?: QueryManagerLike | null
|
|
56
56
|
label?: ValueResolver<T>
|
|
57
57
|
value?: ValueResolver<T>
|
|
58
58
|
transform?: ((options: FilterOption[]) => FilterOption[]) | null
|
|
@@ -62,7 +62,10 @@ export interface FilterQueryOptions<T = unknown> {
|
|
|
62
62
|
/**
|
|
63
63
|
* Minimal interface for EntityManager (to avoid circular deps)
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
// #1191 note: FilterQuery deliberately keeps a self-contained duck type —
|
|
66
|
+
// callers pass minimal mocks ({ _cache, list }) that the full
|
|
67
|
+
// QueryManagerLike would reject. Not a drift case.
|
|
68
|
+
interface QueryManagerLike {
|
|
66
69
|
_cache?: unknown[]
|
|
67
70
|
list(params?: { page_size?: number }): Promise<{ items: unknown[] }>
|
|
68
71
|
}
|
|
@@ -70,8 +73,8 @@ interface EntityManagerLike {
|
|
|
70
73
|
/**
|
|
71
74
|
* Minimal interface for Orchestrator (to avoid circular deps)
|
|
72
75
|
*/
|
|
73
|
-
interface
|
|
74
|
-
get(name: string):
|
|
76
|
+
interface QueryOrchestratorLike {
|
|
77
|
+
get(name: string): QueryManagerLike | null | undefined
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/**
|
|
@@ -97,7 +100,7 @@ export class FilterQuery<T = unknown> {
|
|
|
97
100
|
source: FilterQuerySource
|
|
98
101
|
entity: string | null
|
|
99
102
|
field: string | null
|
|
100
|
-
parentManager:
|
|
103
|
+
parentManager: QueryManagerLike | null
|
|
101
104
|
label: ValueResolver<T>
|
|
102
105
|
value: ValueResolver<T>
|
|
103
106
|
transform: ((options: FilterOption[]) => FilterOption[]) | null
|
|
@@ -146,7 +149,7 @@ export class FilterQuery<T = unknown> {
|
|
|
146
149
|
/**
|
|
147
150
|
* Set the parent manager (called by useListPage for field source)
|
|
148
151
|
*/
|
|
149
|
-
setParentManager(manager:
|
|
152
|
+
setParentManager(manager: QueryManagerLike): this {
|
|
150
153
|
this.parentManager = manager
|
|
151
154
|
return this
|
|
152
155
|
}
|
|
@@ -211,7 +214,7 @@ export class FilterQuery<T = unknown> {
|
|
|
211
214
|
/**
|
|
212
215
|
* Get options for dropdown
|
|
213
216
|
*/
|
|
214
|
-
async getOptions(orchestrator:
|
|
217
|
+
async getOptions(orchestrator: QueryOrchestratorLike | null = null): Promise<FilterOption[]> {
|
|
215
218
|
// Return cached options if available
|
|
216
219
|
if (this._options !== null) {
|
|
217
220
|
return this._options
|
|
@@ -248,7 +251,7 @@ export class FilterQuery<T = unknown> {
|
|
|
248
251
|
/**
|
|
249
252
|
* Fetch items from entity manager
|
|
250
253
|
*/
|
|
251
|
-
private async _fetchFromEntity(orchestrator:
|
|
254
|
+
private async _fetchFromEntity(orchestrator: QueryOrchestratorLike | null): Promise<unknown[]> {
|
|
252
255
|
if (!orchestrator) {
|
|
253
256
|
throw new Error('FilterQuery: orchestrator is required for entity source')
|
|
254
257
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
RoleHierarchyMap,
|
|
6
6
|
RoleProviderContext,
|
|
7
7
|
} from './RolesProvider'
|
|
8
|
+
import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Options for EntityRoleProvider
|
|
@@ -28,19 +29,13 @@ interface RoleEntity {
|
|
|
28
29
|
/**
|
|
29
30
|
* Entity manager interface (minimal for adapter needs)
|
|
30
31
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
create(data: Record<string, unknown>): Promise<RoleEntity>
|
|
34
|
-
update(id: string | number, data: Record<string, unknown>): Promise<RoleEntity>
|
|
35
|
-
delete(id: string | number): Promise<void>
|
|
36
|
-
}
|
|
32
|
+
// #1191 — shared structural view; role-typed results are cast at call sites
|
|
33
|
+
type EntityManager = EntityManagerLike
|
|
37
34
|
|
|
38
35
|
/**
|
|
39
36
|
* Orchestrator interface (minimal for adapter needs)
|
|
40
37
|
*/
|
|
41
|
-
|
|
42
|
-
get(name: string): EntityManager | undefined
|
|
43
|
-
}
|
|
38
|
+
type Orchestrator = OrchestratorLike
|
|
44
39
|
|
|
45
40
|
/**
|
|
46
41
|
* Cache structure for roles
|
|
@@ -162,7 +157,7 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
try {
|
|
165
|
-
const { items: roles } = await manager.list({ page_size: 1000 })
|
|
160
|
+
const { items: roles } = (await manager.list({ page_size: 1000 })) as { items: RoleEntity[] }
|
|
166
161
|
|
|
167
162
|
// Build cache
|
|
168
163
|
const permissions: Record<string, string[]> = {}
|
|
@@ -317,7 +312,7 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
317
312
|
[this._permissionsField]: permissions,
|
|
318
313
|
[this._inheritsField]: inherits,
|
|
319
314
|
}
|
|
320
|
-
const result = await manager.create(data)
|
|
315
|
+
const result = (await manager.create(data)) as RoleEntity
|
|
321
316
|
this.invalidate()
|
|
322
317
|
return result
|
|
323
318
|
}
|
|
@@ -334,10 +329,10 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
334
329
|
const manager = this._getManager()
|
|
335
330
|
|
|
336
331
|
// Find the role entity by name
|
|
337
|
-
const response = await manager.list({
|
|
332
|
+
const response = (await manager.list({
|
|
338
333
|
filter: { [this._nameField]: name },
|
|
339
334
|
limit: 1,
|
|
340
|
-
})
|
|
335
|
+
})) as { items: RoleEntity[]; data?: RoleEntity[] }
|
|
341
336
|
const roles = response.data || response.items
|
|
342
337
|
if (roles.length === 0) {
|
|
343
338
|
throw new Error(`Role '${name}' does not exist`)
|
|
@@ -352,7 +347,7 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
352
347
|
if (permissions !== undefined) updates[this._permissionsField] = permissions
|
|
353
348
|
if (inherits !== undefined) updates[this._inheritsField] = inherits
|
|
354
349
|
|
|
355
|
-
const result = await manager.update(roleEntity.id as string | number, updates)
|
|
350
|
+
const result = (await manager.update(roleEntity.id as string | number, updates)) as RoleEntity
|
|
356
351
|
this.invalidate()
|
|
357
352
|
return result
|
|
358
353
|
}
|
|
@@ -366,10 +361,10 @@ export class EntityRoleProvider extends RoleProvider {
|
|
|
366
361
|
const manager = this._getManager()
|
|
367
362
|
|
|
368
363
|
// Find the role entity by name
|
|
369
|
-
const response = await manager.list({
|
|
364
|
+
const response = (await manager.list({
|
|
370
365
|
filter: { [this._nameField]: name },
|
|
371
366
|
limit: 1,
|
|
372
|
-
})
|
|
367
|
+
})) as { items: RoleEntity[]; data?: RoleEntity[] }
|
|
373
368
|
const roles = response.data || response.items
|
|
374
369
|
if (roles.length === 0) {
|
|
375
370
|
throw new Error(`Role '${name}' does not exist`)
|
package/src/types/index.ts
CHANGED
|
@@ -283,3 +283,19 @@ export interface RequestOptions {
|
|
|
283
283
|
headers?: Record<string, string>
|
|
284
284
|
context?: unknown
|
|
285
285
|
}
|
|
286
|
+
|
|
287
|
+
// ============ UI TYPES ============
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* PrimeVue Button/Tag severity values (#1191) — one shared union instead of
|
|
291
|
+
* `string` + `as any` casts at every :severity binding.
|
|
292
|
+
*/
|
|
293
|
+
export type ButtonSeverity =
|
|
294
|
+
| 'primary'
|
|
295
|
+
| 'secondary'
|
|
296
|
+
| 'success'
|
|
297
|
+
| 'info'
|
|
298
|
+
| 'warn'
|
|
299
|
+
| 'danger'
|
|
300
|
+
| 'help'
|
|
301
|
+
| 'contrast'
|