@quazardous/qdadm 1.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,385 @@
1
+ import { RoleProvider } from './RolesProvider'
2
+ import type {
3
+ RoleMeta,
4
+ RoleData,
5
+ RoleHierarchyMap,
6
+ RoleProviderContext,
7
+ } from './RolesProvider'
8
+
9
+ /**
10
+ * Options for EntityRoleProvider
11
+ */
12
+ export interface EntityRolesProviderOptions {
13
+ entityName?: string
14
+ nameField?: string
15
+ labelField?: string
16
+ permissionsField?: string
17
+ inheritsField?: string
18
+ }
19
+
20
+ /**
21
+ * Role entity structure
22
+ */
23
+ interface RoleEntity {
24
+ id?: string | number
25
+ [key: string]: unknown
26
+ }
27
+
28
+ /**
29
+ * Entity manager interface (minimal for adapter needs)
30
+ */
31
+ interface EntityManager {
32
+ list(params?: Record<string, unknown>): Promise<{ items: RoleEntity[]; data?: RoleEntity[] }>
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
+ }
37
+
38
+ /**
39
+ * Orchestrator interface (minimal for adapter needs)
40
+ */
41
+ interface Orchestrator {
42
+ get(name: string): EntityManager | undefined
43
+ }
44
+
45
+ /**
46
+ * Cache structure for roles
47
+ */
48
+ interface RoleCache {
49
+ permissions: Record<string, string[]>
50
+ hierarchy: Record<string, string[]>
51
+ labels: Record<string, string>
52
+ }
53
+
54
+ /**
55
+ * EntityRoleProvider - Roles provider backed by an entity
56
+ *
57
+ * Fetches roles and permissions from a 'roles' entity.
58
+ * Auto-invalidates cache on entity:roles:* signals.
59
+ * Use this for apps with a role management UI.
60
+ *
61
+ * Expected entity schema:
62
+ * {
63
+ * id: 'role-1',
64
+ * name: 'ROLE_ADMIN',
65
+ * label: 'Administrator',
66
+ * permissions: ['entity:**', 'admin:**'],
67
+ * inherits: ['ROLE_USER']
68
+ * }
69
+ *
70
+ * @example
71
+ * // Configure Kernel with entity-based roles
72
+ * const kernel = new Kernel({
73
+ * modules: [SecurityModule, ...],
74
+ * security: {
75
+ * rolesProvider: new EntityRoleProvider({
76
+ * entityName: 'roles',
77
+ * nameField: 'name',
78
+ * permissionsField: 'permissions',
79
+ * inheritsField: 'inherits'
80
+ * })
81
+ * }
82
+ * })
83
+ *
84
+ * // Load roles before boot
85
+ * await kernel.options.security.rolesProvider.load()
86
+ * await kernel.boot()
87
+ */
88
+ export class EntityRoleProvider extends RoleProvider {
89
+ private _entityName: string
90
+ private _nameField: string
91
+ private _labelField: string
92
+ private _permissionsField: string
93
+ private _inheritsField: string
94
+
95
+ private _cache: RoleCache | null = null
96
+ private _ctx: RoleProviderContext | null = null
97
+ private _orchestrator: Orchestrator | null = null
98
+ private _signalCleanup: (() => void) | null = null
99
+ private _loading: Promise<void> | null = null
100
+
101
+ constructor(options: EntityRolesProviderOptions = {}) {
102
+ super()
103
+ this._entityName = options.entityName || 'roles'
104
+ this._nameField = options.nameField || 'name'
105
+ this._labelField = options.labelField || 'label'
106
+ this._permissionsField = options.permissionsField || 'permissions'
107
+ this._inheritsField = options.inheritsField || 'inherits'
108
+ }
109
+
110
+ /**
111
+ * Install adapter (called by Kernel after orchestrator ready)
112
+ */
113
+ install(ctx: RoleProviderContext): void {
114
+ this._ctx = ctx
115
+ this._orchestrator = ctx.orchestrator as Orchestrator
116
+
117
+ // Auto-invalidate on role changes
118
+ if (ctx.signals) {
119
+ this._signalCleanup = ctx.signals.on(`entity:${this._entityName}:**`, () => {
120
+ this.invalidate()
121
+ })
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Uninstall adapter (cleanup)
127
+ */
128
+ uninstall(): void {
129
+ if (this._signalCleanup) {
130
+ this._signalCleanup()
131
+ this._signalCleanup = null
132
+ }
133
+ this._ctx = null
134
+ this._orchestrator = null
135
+ this._cache = null
136
+ this._loading = null
137
+ }
138
+
139
+ /**
140
+ * Load roles from entity (async, must be called before use)
141
+ */
142
+ async load(): Promise<void> {
143
+ // Prevent concurrent loads
144
+ if (this._loading) {
145
+ return this._loading
146
+ }
147
+
148
+ this._loading = this._doLoad()
149
+ await this._loading
150
+ this._loading = null
151
+ }
152
+
153
+ /**
154
+ * Internal load implementation
155
+ */
156
+ private async _doLoad(): Promise<void> {
157
+ const manager = this._orchestrator?.get(this._entityName)
158
+ if (!manager) {
159
+ console.warn(`[EntityRolesProvider] Entity '${this._entityName}' not found`)
160
+ this._cache = { permissions: {}, hierarchy: {}, labels: {} }
161
+ return
162
+ }
163
+
164
+ try {
165
+ const { items: roles } = await manager.list({ page_size: 1000 })
166
+
167
+ // Build cache
168
+ const permissions: Record<string, string[]> = {}
169
+ const hierarchy: Record<string, string[]> = {}
170
+ const labels: Record<string, string> = {}
171
+
172
+ for (const role of roles) {
173
+ const name = role[this._nameField] as string
174
+ if (!name) continue
175
+
176
+ permissions[name] = (role[this._permissionsField] as string[]) || []
177
+ labels[name] = (role[this._labelField] as string) || name
178
+
179
+ const inherits = role[this._inheritsField] as string[] | undefined
180
+ if (inherits && inherits.length > 0) {
181
+ hierarchy[name] = inherits
182
+ }
183
+ }
184
+
185
+ this._cache = { permissions, hierarchy, labels }
186
+ } catch (error) {
187
+ console.error(`[EntityRolesProvider] Failed to load roles:`, error)
188
+ this._cache = { permissions: {}, hierarchy: {}, labels: {} }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Get cache, throwing if not loaded
194
+ */
195
+ private _getCache(): RoleCache {
196
+ if (!this._cache) {
197
+ throw new Error(
198
+ '[EntityRolesProvider] Roles not loaded. Call load() before using the adapter.'
199
+ )
200
+ }
201
+ return this._cache
202
+ }
203
+
204
+ /**
205
+ * Get permissions for a role
206
+ */
207
+ getPermissions(role: string): string[] {
208
+ const cache = this._getCache()
209
+ return cache.permissions[role] || []
210
+ }
211
+
212
+ /**
213
+ * Get all defined roles
214
+ */
215
+ getRoles(): string[] {
216
+ const cache = this._getCache()
217
+ return Object.keys(cache.permissions)
218
+ }
219
+
220
+ /**
221
+ * Get role hierarchy
222
+ */
223
+ getHierarchy(): RoleHierarchyMap {
224
+ const cache = this._getCache()
225
+ return cache.hierarchy
226
+ }
227
+
228
+ /**
229
+ * Get role metadata
230
+ */
231
+ getRoleMeta(role: string): RoleMeta | null {
232
+ const cache = this._getCache()
233
+ const label = cache.labels[role]
234
+ if (!label) return null
235
+ return { label }
236
+ }
237
+
238
+ /**
239
+ * Invalidate cache (triggers reload on next access)
240
+ * Call this after role changes
241
+ */
242
+ invalidate(): void {
243
+ this._cache = null
244
+ }
245
+
246
+ /**
247
+ * Check if cache is loaded
248
+ */
249
+ get isLoaded(): boolean {
250
+ return this._cache !== null
251
+ }
252
+
253
+ /**
254
+ * Entity-backed adapter can always persist (via entity manager)
255
+ */
256
+ get canPersist(): boolean {
257
+ return true
258
+ }
259
+
260
+ // ─────────────────────────────────────────────────────────────────────────────
261
+ // Role query methods
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+
264
+ /**
265
+ * Check if a role exists
266
+ */
267
+ roleExists(role: string): boolean {
268
+ const cache = this._getCache()
269
+ return cache.permissions[role] !== undefined
270
+ }
271
+
272
+ /**
273
+ * Get complete role object
274
+ */
275
+ getRole(role: string): RoleData | null {
276
+ const cache = this._getCache()
277
+ if (cache.permissions[role] === undefined) {
278
+ return null
279
+ }
280
+ return {
281
+ name: role,
282
+ label: cache.labels[role] || role,
283
+ permissions: cache.permissions[role] || [],
284
+ inherits: cache.hierarchy[role] || [],
285
+ }
286
+ }
287
+
288
+ // ─────────────────────────────────────────────────────────────────────────────
289
+ // Mutation methods (via entity manager)
290
+ // ─────────────────────────────────────────────────────────────────────────────
291
+
292
+ /**
293
+ * Get the entity manager for roles
294
+ */
295
+ private _getManager(): EntityManager {
296
+ const manager = this._orchestrator?.get(this._entityName)
297
+ if (!manager) {
298
+ throw new Error(`Entity '${this._entityName}' not found`)
299
+ }
300
+ return manager
301
+ }
302
+
303
+ /**
304
+ * Create a new role
305
+ */
306
+ async createRole(
307
+ name: string,
308
+ { label, permissions = [], inherits = [] }: { label?: string; permissions?: string[]; inherits?: string[] } = {}
309
+ ): Promise<RoleEntity> {
310
+ if (this.roleExists(name)) {
311
+ throw new Error(`Role '${name}' already exists`)
312
+ }
313
+ const manager = this._getManager()
314
+ const data = {
315
+ [this._nameField]: name,
316
+ [this._labelField]: label || name,
317
+ [this._permissionsField]: permissions,
318
+ [this._inheritsField]: inherits,
319
+ }
320
+ const result = await manager.create(data)
321
+ this.invalidate()
322
+ return result
323
+ }
324
+
325
+ /**
326
+ * Update an existing role
327
+ */
328
+ async updateRole(
329
+ name: string,
330
+ { label, permissions, inherits }: { label?: string; permissions?: string[]; inherits?: string[] } = {}
331
+ ): Promise<RoleEntity> {
332
+ // Ensure loaded first
333
+ this._getCache()
334
+ const manager = this._getManager()
335
+
336
+ // Find the role entity by name
337
+ const response = await manager.list({
338
+ filter: { [this._nameField]: name },
339
+ limit: 1,
340
+ })
341
+ const roles = response.data || response.items
342
+ if (roles.length === 0) {
343
+ throw new Error(`Role '${name}' does not exist`)
344
+ }
345
+
346
+ const roleEntity = roles[0]
347
+ if (!roleEntity) {
348
+ throw new Error(`Role '${name}' does not exist`)
349
+ }
350
+ const updates: Record<string, unknown> = {}
351
+ if (label !== undefined) updates[this._labelField] = label
352
+ if (permissions !== undefined) updates[this._permissionsField] = permissions
353
+ if (inherits !== undefined) updates[this._inheritsField] = inherits
354
+
355
+ const result = await manager.update(roleEntity.id as string | number, updates)
356
+ this.invalidate()
357
+ return result
358
+ }
359
+
360
+ /**
361
+ * Delete a role
362
+ */
363
+ async deleteRole(name: string): Promise<void> {
364
+ // Ensure loaded first
365
+ this._getCache()
366
+ const manager = this._getManager()
367
+
368
+ // Find the role entity by name
369
+ const response = await manager.list({
370
+ filter: { [this._nameField]: name },
371
+ limit: 1,
372
+ })
373
+ const roles = response.data || response.items
374
+ if (roles.length === 0) {
375
+ throw new Error(`Role '${name}' does not exist`)
376
+ }
377
+
378
+ const roleToDelete = roles[0]
379
+ if (!roleToDelete) {
380
+ throw new Error(`Role '${name}' does not exist`)
381
+ }
382
+ await manager.delete(roleToDelete.id as string | number)
383
+ this.invalidate()
384
+ }
385
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * PermissionMatcher - Wildcard permission matching
3
+ *
4
+ * Supports two wildcard patterns (similar to signals):
5
+ * - `*` matches exactly one segment
6
+ * - `**` matches zero or more segments (greedy)
7
+ *
8
+ * Permission format: namespace:target:action
9
+ * Examples:
10
+ * entity:books:read - specific permission
11
+ * entity:books:* - any action on books
12
+ * entity:*:read - read on any entity
13
+ * entity:** - all entity permissions
14
+ * ** - super admin (matches everything)
15
+ *
16
+ * @example
17
+ * PermissionMatcher.matches('entity:*:read', 'entity:books:read') // true
18
+ * PermissionMatcher.matches('entity:**', 'entity:books:read') // true
19
+ * PermissionMatcher.matches('**', 'anything:here') // true
20
+ */
21
+ export class PermissionMatcher {
22
+ /**
23
+ * Check if a permission pattern matches a required permission
24
+ *
25
+ * @param pattern - Pattern with wildcards (e.g., 'entity:*:read')
26
+ * @param required - Required permission (e.g., 'entity:books:read')
27
+ * @returns True if pattern matches required
28
+ *
29
+ * @example
30
+ * PermissionMatcher.matches('entity:books:read', 'entity:books:read') // true (exact)
31
+ * PermissionMatcher.matches('entity:books:*', 'entity:books:read') // true (* = one segment)
32
+ * PermissionMatcher.matches('entity:*:read', 'entity:books:read') // true
33
+ * PermissionMatcher.matches('entity:**', 'entity:books:read') // true (** = greedy)
34
+ * PermissionMatcher.matches('**', 'entity:books:read') // true (super admin)
35
+ * PermissionMatcher.matches('entity:*:read', 'entity:a:b:read') // false (* = exactly one)
36
+ */
37
+ static matches(pattern: string, required: string): boolean {
38
+ // Super admin
39
+ if (pattern === '**') return true
40
+
41
+ // Exact match
42
+ if (pattern === required) return true
43
+
44
+ const patternParts = pattern.split(':')
45
+ const requiredParts = required.split(':')
46
+
47
+ return this._matchParts(patternParts, requiredParts, 0, 0)
48
+ }
49
+
50
+ /**
51
+ * Recursive matching of pattern parts against required parts
52
+ */
53
+ private static _matchParts(
54
+ pattern: string[],
55
+ required: string[],
56
+ pi: number,
57
+ ri: number
58
+ ): boolean {
59
+ // Both exhausted = match
60
+ if (pi === pattern.length && ri === required.length) {
61
+ return true
62
+ }
63
+
64
+ // Pattern exhausted but required has more = no match
65
+ if (pi === pattern.length) {
66
+ return false
67
+ }
68
+
69
+ const p = pattern[pi]
70
+
71
+ // ** matches zero or more remaining segments
72
+ if (p === '**') {
73
+ // ** at end matches everything remaining
74
+ if (pi === pattern.length - 1) {
75
+ return true
76
+ }
77
+
78
+ // Try matching ** against 0, 1, 2, ... segments
79
+ for (let skip = 0; skip <= required.length - ri; skip++) {
80
+ if (this._matchParts(pattern, required, pi + 1, ri + skip)) {
81
+ return true
82
+ }
83
+ }
84
+ return false
85
+ }
86
+
87
+ // Required exhausted but pattern has more (and not **)
88
+ if (ri === required.length) {
89
+ return false
90
+ }
91
+
92
+ // * matches exactly one segment
93
+ if (p === '*') {
94
+ return this._matchParts(pattern, required, pi + 1, ri + 1)
95
+ }
96
+
97
+ // Literal match
98
+ if (p === required[ri]) {
99
+ return this._matchParts(pattern, required, pi + 1, ri + 1)
100
+ }
101
+
102
+ return false
103
+ }
104
+
105
+ /**
106
+ * Check if any pattern in array matches the required permission
107
+ *
108
+ * @param patterns - Array of patterns (user's permissions)
109
+ * @param required - Required permission to check
110
+ * @returns True if any pattern matches
111
+ *
112
+ * @example
113
+ * const userPerms = ['entity:*:read', 'entity:*:list', 'auth:impersonate']
114
+ * PermissionMatcher.any(userPerms, 'entity:books:read') // true
115
+ * PermissionMatcher.any(userPerms, 'entity:books:delete') // false
116
+ */
117
+ static any(patterns: string[] | null | undefined, required: string): boolean {
118
+ if (!patterns || patterns.length === 0) return false
119
+ return patterns.some((p) => this.matches(p, required))
120
+ }
121
+
122
+ /**
123
+ * Filter permissions that match a pattern
124
+ *
125
+ * @param permissions - Array of specific permissions
126
+ * @param pattern - Pattern to filter by
127
+ * @returns Permissions that match the pattern
128
+ *
129
+ * @example
130
+ * const allPerms = ['entity:books:read', 'entity:books:create', 'auth:login']
131
+ * PermissionMatcher.filter(allPerms, 'entity:books:*') // ['entity:books:read', 'entity:books:create']
132
+ */
133
+ static filter(permissions: string[], pattern: string): string[] {
134
+ return permissions.filter((p) => this.matches(pattern, p))
135
+ }
136
+
137
+ /**
138
+ * Expand a pattern against registered permissions
139
+ * Useful for UI: show what a wildcard pattern actually grants
140
+ *
141
+ * @param pattern - Pattern to expand
142
+ * @param allPermissions - All registered permissions
143
+ * @returns Permissions that the pattern would grant
144
+ *
145
+ * @example
146
+ * const registry = ['entity:books:read', 'entity:books:create', 'entity:loans:read']
147
+ * PermissionMatcher.expand('entity:*:read', registry) // ['entity:books:read', 'entity:loans:read']
148
+ */
149
+ static expand(pattern: string, allPermissions: string[]): string[] {
150
+ return allPermissions.filter((p) => this.matches(pattern, p))
151
+ }
152
+ }