qdadm 0.32.0 → 0.34.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qdadm",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@
20
20
  },
21
21
  "exports": {
22
22
  ".": "./src/index.js",
23
+ "./auth": "./src/auth/index.js",
23
24
  "./composables": "./src/composables/index.js",
24
25
  "./components": "./src/components/index.js",
25
26
  "./editors": "./src/editors/index.js",
@@ -0,0 +1,254 @@
1
+ /**
2
+ * SessionAuthAdapter - Base class for user authentication
3
+ *
4
+ * Applications extend this class to implement their authentication logic.
5
+ * The adapter handles user sessions: login, logout, token management.
6
+ *
7
+ * This is different from EntityAuthAdapter which handles entity-level permissions.
8
+ *
9
+ * @example
10
+ * ```js
11
+ * class MyAuthAdapter extends SessionAuthAdapter {
12
+ * async login({ username, password }) {
13
+ * const response = await api.post('/auth/login', { username, password })
14
+ * this.setSession(response.token, response.user)
15
+ * return { token: response.token, user: response.user }
16
+ * }
17
+ *
18
+ * logout() {
19
+ * this.clearSession()
20
+ * }
21
+ * }
22
+ * ```
23
+ *
24
+ * @abstract
25
+ */
26
+ export class SessionAuthAdapter {
27
+ /**
28
+ * Internal session state
29
+ * @protected
30
+ */
31
+ _token = null
32
+ _user = null
33
+
34
+ /**
35
+ * Authenticate user with credentials
36
+ *
37
+ * @param {object} credentials - Login credentials
38
+ * @param {string} credentials.username - Username or email
39
+ * @param {string} credentials.password - Password
40
+ * @returns {Promise<{token: string, user: object}>} Session data
41
+ * @throws {Error} If authentication fails
42
+ *
43
+ * @example
44
+ * const { token, user } = await adapter.login({ username: 'admin', password: 'secret' })
45
+ */
46
+ async login(credentials) {
47
+ throw new Error('[SessionAuthAdapter] login() must be implemented by subclass')
48
+ }
49
+
50
+ /**
51
+ * End the current session
52
+ *
53
+ * Should clear all session data (tokens, user info).
54
+ * Called by AppLayout logout button and useAuth().logout()
55
+ */
56
+ logout() {
57
+ throw new Error('[SessionAuthAdapter] logout() must be implemented by subclass')
58
+ }
59
+
60
+ /**
61
+ * Check if user is currently authenticated
62
+ *
63
+ * @returns {boolean} True if user has valid session
64
+ *
65
+ * @example
66
+ * if (adapter.isAuthenticated()) {
67
+ * // Show dashboard
68
+ * } else {
69
+ * // Redirect to login
70
+ * }
71
+ */
72
+ isAuthenticated() {
73
+ throw new Error('[SessionAuthAdapter] isAuthenticated() must be implemented by subclass')
74
+ }
75
+
76
+ /**
77
+ * Get the current authentication token
78
+ *
79
+ * Used by API clients to include in request headers.
80
+ *
81
+ * @returns {string|null} JWT token or null if not authenticated
82
+ *
83
+ * @example
84
+ * const token = adapter.getToken()
85
+ * fetch('/api/data', {
86
+ * headers: { Authorization: `Bearer ${token}` }
87
+ * })
88
+ */
89
+ getToken() {
90
+ throw new Error('[SessionAuthAdapter] getToken() must be implemented by subclass')
91
+ }
92
+
93
+ /**
94
+ * Get the current user object
95
+ *
96
+ * Returns user data from the session. The shape depends on your backend.
97
+ *
98
+ * @returns {object|null} User object or null if not authenticated
99
+ *
100
+ * @example
101
+ * const user = adapter.getUser()
102
+ * console.log(user.username, user.email, user.roles)
103
+ */
104
+ getUser() {
105
+ throw new Error('[SessionAuthAdapter] getUser() must be implemented by subclass')
106
+ }
107
+
108
+ /**
109
+ * Synchronous user getter (optional)
110
+ *
111
+ * Some implementations prefer a property instead of method.
112
+ * useAuth() supports both patterns.
113
+ *
114
+ * @type {object|null}
115
+ */
116
+ get user() {
117
+ return this.getUser()
118
+ }
119
+
120
+ // ─────────────────────────────────────────────────────────────────
121
+ // Helper methods for subclasses
122
+ // ─────────────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Set session data (helper for subclasses)
126
+ *
127
+ * @param {string} token - Authentication token
128
+ * @param {object} user - User object
129
+ * @protected
130
+ */
131
+ setSession(token, user) {
132
+ this._token = token
133
+ this._user = user
134
+ }
135
+
136
+ /**
137
+ * Clear session data (helper for subclasses)
138
+ *
139
+ * @protected
140
+ */
141
+ clearSession() {
142
+ this._token = null
143
+ this._user = null
144
+ }
145
+
146
+ /**
147
+ * Validate that the adapter is properly configured
148
+ *
149
+ * Called during bootstrap to catch configuration errors early.
150
+ *
151
+ * @throws {Error} If required methods are not implemented
152
+ */
153
+ static validate(adapter) {
154
+ const required = ['login', 'logout', 'isAuthenticated', 'getToken', 'getUser']
155
+ const missing = required.filter(method => typeof adapter[method] !== 'function')
156
+
157
+ if (missing.length > 0) {
158
+ throw new Error(
159
+ `[SessionAuthAdapter] Missing required methods: ${missing.join(', ')}\n` +
160
+ 'Ensure your authAdapter implements all required methods or extends SessionAuthAdapter.'
161
+ )
162
+ }
163
+ }
164
+ }
165
+
166
+ /**
167
+ * LocalStorage-based SessionAuthAdapter implementation
168
+ *
169
+ * Ready-to-use adapter that stores session in localStorage.
170
+ * Extend and override login() to add your API call.
171
+ *
172
+ * @example
173
+ * ```js
174
+ * class MyAuthAdapter extends LocalStorageSessionAuthAdapter {
175
+ * constructor() {
176
+ * super('my_app_auth') // localStorage key
177
+ * }
178
+ *
179
+ * async login({ username, password }) {
180
+ * const res = await fetch('/api/login', {
181
+ * method: 'POST',
182
+ * body: JSON.stringify({ username, password })
183
+ * })
184
+ * const data = await res.json()
185
+ * this.setSession(data.token, data.user)
186
+ * this.persist()
187
+ * return data
188
+ * }
189
+ * }
190
+ * ```
191
+ */
192
+ export class LocalStorageSessionAuthAdapter extends SessionAuthAdapter {
193
+ /**
194
+ * @param {string} storageKey - localStorage key for session data
195
+ */
196
+ constructor(storageKey = 'qdadm_auth') {
197
+ super()
198
+ this._storageKey = storageKey
199
+ this._restore()
200
+ }
201
+
202
+ /**
203
+ * Restore session from localStorage on init
204
+ * @private
205
+ */
206
+ _restore() {
207
+ try {
208
+ const stored = localStorage.getItem(this._storageKey)
209
+ if (stored) {
210
+ const { token, user } = JSON.parse(stored)
211
+ this._token = token
212
+ this._user = user
213
+ }
214
+ } catch {
215
+ // Invalid stored data, ignore
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Persist session to localStorage
221
+ * Call after login() to save session
222
+ * @protected
223
+ */
224
+ persist() {
225
+ if (this._token && this._user) {
226
+ localStorage.setItem(this._storageKey, JSON.stringify({
227
+ token: this._token,
228
+ user: this._user
229
+ }))
230
+ } else {
231
+ localStorage.removeItem(this._storageKey)
232
+ }
233
+ }
234
+
235
+ // Arrow functions to preserve `this` when used as callbacks
236
+ logout = () => {
237
+ this.clearSession()
238
+ localStorage.removeItem(this._storageKey)
239
+ }
240
+
241
+ isAuthenticated = () => {
242
+ return !!this._token
243
+ }
244
+
245
+ getToken = () => {
246
+ return this._token
247
+ }
248
+
249
+ getUser = () => {
250
+ return this._user
251
+ }
252
+ }
253
+
254
+ export default SessionAuthAdapter
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Auth module - User session authentication
3
+ *
4
+ * For entity-level permissions, see entity/auth/EntityAuthAdapter
5
+ */
6
+
7
+ export {
8
+ SessionAuthAdapter,
9
+ LocalStorageSessionAuthAdapter,
10
+ } from './SessionAuthAdapter.js'
@@ -35,6 +35,7 @@ const route = useRoute()
35
35
  const app = useApp()
36
36
  const { navSections, isNavActive, sectionHasActiveItem, handleNavClick } = useNavigation()
37
37
  const { isAuthenticated, user, logout, authEnabled } = useAuth()
38
+ const signals = inject('qdadmSignals', null)
38
39
 
39
40
  // LocalStorage key for collapsed sections state (namespaced by app)
40
41
  const STORAGE_KEY = computed(() => `${app.shortName.toLowerCase()}_nav_collapsed`)
@@ -150,6 +151,7 @@ const userSubtitle = computed(() => {
150
151
 
151
152
  function handleLogout() {
152
153
  logout()
154
+ signals?.emit('auth:logout', { reason: 'user' })
153
155
  router.push({ name: 'login' })
154
156
  }
155
157
 
@@ -90,11 +90,12 @@ const props = defineProps({
90
90
  default: ''
91
91
  },
92
92
  /**
93
- * Emit business signal on login (requires orchestrator)
93
+ * Emit auth:login signal on successful login
94
+ * Required for debug bar auth tracking and other signal listeners
94
95
  */
95
96
  emitSignal: {
96
97
  type: Boolean,
97
- default: false
98
+ default: true
98
99
  }
99
100
  })
100
101
 
@@ -120,7 +120,13 @@ export function useFormPageBuilder(config = {}) {
120
120
  // Get EntityManager via orchestrator
121
121
  const orchestrator = inject('qdadmOrchestrator')
122
122
  if (!orchestrator) {
123
- throw new Error('[qdadm] Orchestrator not provided. Make sure to use createQdadm() with entityFactory.')
123
+ throw new Error(
124
+ '[qdadm] Orchestrator not provided.\n' +
125
+ 'Possible causes:\n' +
126
+ '1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
127
+ '2. Component used outside of qdadm app context\n' +
128
+ '3. Missing entityFactory in Kernel options'
129
+ )
124
130
  }
125
131
  const manager = orchestrator.get(entity)
126
132
 
@@ -143,7 +143,13 @@ export function useListPageBuilder(config = {}) {
143
143
  // Get EntityManager via orchestrator
144
144
  const orchestrator = inject('qdadmOrchestrator')
145
145
  if (!orchestrator) {
146
- throw new Error('[qdadm] Orchestrator not provided. Make sure to use createQdadm() with entityFactory.')
146
+ throw new Error(
147
+ '[qdadm] Orchestrator not provided.\n' +
148
+ 'Possible causes:\n' +
149
+ '1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
150
+ '2. Component used outside of qdadm app context\n' +
151
+ '3. Missing entityFactory in Kernel options'
152
+ )
147
153
  }
148
154
  const manager = orchestrator.get(entity)
149
155
 
@@ -179,10 +179,11 @@ export class Collector {
179
179
 
180
180
  /**
181
181
  * Get all entries
182
+ * Returns a shallow copy to trigger Vue reactivity when used in computed
182
183
  * @returns {Array<object>} All recorded entries
183
184
  */
184
185
  getEntries() {
185
- return this.entries
186
+ return [...this.entries]
186
187
  }
187
188
 
188
189
  /**
@@ -1,5 +1,5 @@
1
1
  import { PermissiveAuthAdapter } from './auth/PermissiveAdapter.js'
2
- import { AuthActions } from './auth/AuthAdapter.js'
2
+ import { AuthActions } from './auth/EntityAuthAdapter.js'
3
3
  import { QueryExecutor } from '../query/QueryExecutor.js'
4
4
 
5
5
  /**
@@ -26,10 +26,10 @@
26
26
  * ```
27
27
  */
28
28
 
29
- import { AuthAdapter } from './AuthAdapter.js'
29
+ import { EntityAuthAdapter } from './EntityAuthAdapter.js'
30
30
  import { authFactory } from './factory.js'
31
31
 
32
- export class CompositeAuthAdapter extends AuthAdapter {
32
+ export class CompositeAuthAdapter extends EntityAuthAdapter {
33
33
  /**
34
34
  * @param {object} config - Composite auth configuration
35
35
  * @param {AuthAdapter|string|object} config.default - Default adapter (required)
@@ -1,7 +1,7 @@
1
1
  /**
2
- * AuthAdapter - Interface for scope and silo permission checks
2
+ * EntityAuthAdapter - Interface for entity-level permission checks
3
3
  *
4
- * Applications implement this interface to plug their authentication/authorization
4
+ * Applications implement this interface to plug their authorization
5
5
  * system into qdadm's EntityManager. The adapter provides two levels of permission checks:
6
6
  *
7
7
  * 1. **Scopes** (action-level): Can the user perform this action on this entity type?
@@ -10,9 +10,11 @@
10
10
  * 2. **Silos** (record-level): Can the user access this specific record?
11
11
  * Example: Can user see invoice #123? (ownership, team membership, etc.)
12
12
  *
13
+ * Note: For user session authentication (login/logout), see SessionAuthAdapter.
14
+ *
13
15
  * Usage:
14
16
  * ```js
15
- * class MyAuthAdapter extends AuthAdapter {
17
+ * class MyEntityAuthAdapter extends EntityAuthAdapter {
16
18
  * canPerform(entity, action) {
17
19
  * const user = this.getCurrentUser()
18
20
  * return user?.permissions?.includes(`${entity}:${action}`)
@@ -20,7 +22,6 @@
20
22
  *
21
23
  * canAccessRecord(entity, record) {
22
24
  * const user = this.getCurrentUser()
23
- * // Check ownership or team membership
24
25
  * return record.owner_id === user?.id || record.team_id === user?.team_id
25
26
  * }
26
27
  *
@@ -32,7 +33,7 @@
32
33
  *
33
34
  * @interface
34
35
  */
35
- export class AuthAdapter {
36
+ export class EntityAuthAdapter {
36
37
  /**
37
38
  * SecurityChecker instance for isGranted() delegation
38
39
  * @type {import('./SecurityChecker.js').SecurityChecker|null}
@@ -104,14 +105,11 @@ export class AuthAdapter {
104
105
  * @returns {boolean} True if user can perform the action on this entity type
105
106
  *
106
107
  * @example
107
- * // Check if user can create invoices
108
108
  * adapter.canPerform('invoices', 'create') // true/false
109
- *
110
- * // Check if user can delete users
111
109
  * adapter.canPerform('users', 'delete') // true/false
112
110
  */
113
111
  canPerform(entity, action) {
114
- throw new Error('[AuthAdapter] canPerform() must be implemented by subclass')
112
+ throw new Error('[EntityAuthAdapter] canPerform() must be implemented by subclass')
115
113
  }
116
114
 
117
115
  /**
@@ -121,52 +119,27 @@ export class AuthAdapter {
121
119
  * this determines if the user can access a particular record based on
122
120
  * ownership, team membership, or other business rules.
123
121
  *
124
- * Called during:
125
- * - get() operations
126
- * - update() / patch() operations
127
- * - delete() operations
128
- * - list() result filtering (optional)
129
- *
130
122
  * @param {string} entity - Entity name (e.g., 'users', 'invoices')
131
123
  * @param {object} record - The full entity record to check access for
132
124
  * @returns {boolean} True if user can access this specific record
133
125
  *
134
126
  * @example
135
- * // Check if user can access a specific invoice
136
127
  * adapter.canAccessRecord('invoices', { id: 123, owner_id: 456, ... })
137
- *
138
- * @example
139
- * // Common implementations:
140
- * // 1. Ownership: record.owner_id === user.id
141
- * // 2. Team: record.team_id === user.team_id
142
- * // 3. Role: user.role === 'admin' (admins see all)
143
- * // 4. Hierarchical: record.organization_id in user.organizations
144
128
  */
145
129
  canAccessRecord(entity, record) {
146
- throw new Error('[AuthAdapter] canAccessRecord() must be implemented by subclass')
130
+ throw new Error('[EntityAuthAdapter] canAccessRecord() must be implemented by subclass')
147
131
  }
148
132
 
149
133
  /**
150
134
  * Get the current authenticated user
151
135
  *
152
136
  * Returns the user object or null if not authenticated. The user object
153
- * should contain whatever information is needed for permission checks
154
- * (id, role, team_id, permissions array, etc.).
137
+ * should contain whatever information is needed for permission checks.
155
138
  *
156
139
  * @returns {object|null} Current user object or null if not authenticated
157
- *
158
- * @example
159
- * // Typical user object:
160
- * {
161
- * id: 123,
162
- * email: 'user@example.com',
163
- * role: 'manager',
164
- * team_id: 456,
165
- * permissions: ['invoices:create', 'invoices:read', 'users:read']
166
- * }
167
140
  */
168
141
  getCurrentUser() {
169
- throw new Error('[AuthAdapter] getCurrentUser() must be implemented by subclass')
142
+ throw new Error('[EntityAuthAdapter] getCurrentUser() must be implemented by subclass')
170
143
  }
171
144
  }
172
145
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * PermissiveAuthAdapter - Default adapter that allows all operations
3
3
  *
4
- * This adapter is used when no custom AuthAdapter is provided to EntityManager.
4
+ * This adapter is used when no custom EntityAuthAdapter is provided to EntityManager.
5
5
  * It returns `true` for all permission checks, effectively disabling authorization.
6
6
  *
7
7
  * Use cases:
@@ -11,18 +11,16 @@
11
11
  * - Testing environments
12
12
  *
13
13
  * @example
14
- * // Explicit usage (rarely needed)
15
14
  * const adapter = new PermissiveAuthAdapter()
16
- *
17
15
  * adapter.canPerform('users', 'delete') // true
18
16
  * adapter.canAccessRecord('invoices', { id: 123, secret: true }) // true
19
17
  * adapter.getCurrentUser() // null
20
18
  *
21
- * @extends AuthAdapter
19
+ * @extends EntityAuthAdapter
22
20
  */
23
- import { AuthAdapter } from './AuthAdapter.js'
21
+ import { EntityAuthAdapter } from './EntityAuthAdapter.js'
24
22
 
25
- export class PermissiveAuthAdapter extends AuthAdapter {
23
+ export class PermissiveAuthAdapter extends EntityAuthAdapter {
26
24
  /**
27
25
  * Always allows any action on any entity type
28
26
  *
@@ -27,13 +27,13 @@
27
27
  * ```
28
28
  */
29
29
 
30
- import { AuthAdapter } from './AuthAdapter.js'
30
+ import { EntityAuthAdapter } from './EntityAuthAdapter.js'
31
31
  import { PermissiveAuthAdapter } from './PermissiveAdapter.js'
32
32
 
33
33
  /**
34
34
  * Built-in auth adapter types
35
35
  * Extended via context.authTypes for custom adapters
36
- * @type {Record<string, typeof AuthAdapter>}
36
+ * @type {Record<string, typeof EntityAuthAdapter>}
37
37
  */
38
38
  export const authTypes = {
39
39
  permissive: PermissiveAuthAdapter
@@ -65,7 +65,7 @@ export function parseAuthPattern(pattern) {
65
65
  *
66
66
  * @param {object} config - Normalized auth config with `type` property
67
67
  * @param {object} context - Context with authTypes registry
68
- * @returns {AuthAdapter} Adapter instance
68
+ * @returns {EntityAuthAdapter} Adapter instance
69
69
  */
70
70
  export function defaultAuthResolver(config, context = {}) {
71
71
  const { type, ...rest } = config
@@ -109,9 +109,9 @@ function isCompositeConfig(config) {
109
109
  * - Config object with 'type' → resolve via registry
110
110
  * - Config object with 'default' → create CompositeAuthAdapter
111
111
  *
112
- * @param {AuthAdapter | string | object} config - Auth config
112
+ * @param {EntityAuthAdapter | string | object} config - Auth config
113
113
  * @param {object} [context={}] - Context with authTypes, authResolver
114
- * @returns {AuthAdapter} Adapter instance
114
+ * @returns {EntityAuthAdapter} Adapter instance
115
115
  *
116
116
  * @example
117
117
  * // Instance passthrough (most common, backward compatible)
@@ -138,8 +138,8 @@ export function authFactory(config, context = {}) {
138
138
  return new PermissiveAuthAdapter()
139
139
  }
140
140
 
141
- // Already an AuthAdapter instance → return directly (backward compatible)
142
- if (config instanceof AuthAdapter) {
141
+ // Already an EntityAuthAdapter instance → return directly (backward compatible)
142
+ if (config instanceof EntityAuthAdapter) {
143
143
  return config
144
144
  }
145
145
 
@@ -2,13 +2,13 @@
2
2
  * Tests for auth factory and CompositeAuthAdapter
3
3
  */
4
4
  import { describe, it, expect } from 'vitest'
5
- import { AuthAdapter } from './AuthAdapter.js'
5
+ import { EntityAuthAdapter } from './EntityAuthAdapter.js'
6
6
  import { PermissiveAuthAdapter } from './PermissiveAdapter.js'
7
7
  import { CompositeAuthAdapter } from './CompositeAuthAdapter.js'
8
8
  import { authFactory, parseAuthPattern, authTypes } from './factory.js'
9
9
 
10
10
  // Test adapter for custom types
11
- class TestAuthAdapter extends AuthAdapter {
11
+ class TestAuthAdapter extends EntityAuthAdapter {
12
12
  constructor(options = {}) {
13
13
  super()
14
14
  this.options = options
@@ -19,7 +19,7 @@ class TestAuthAdapter extends AuthAdapter {
19
19
  }
20
20
 
21
21
  // Adapter that tracks which entity was checked
22
- class TrackingAdapter extends AuthAdapter {
22
+ class TrackingAdapter extends EntityAuthAdapter {
23
23
  constructor(name) {
24
24
  super()
25
25
  this.name = name
@@ -53,7 +53,7 @@ describe('parseAuthPattern', () => {
53
53
 
54
54
  describe('authFactory', () => {
55
55
  describe('instance passthrough', () => {
56
- it('returns AuthAdapter instance as-is', () => {
56
+ it('returns EntityAuthAdapter instance as-is', () => {
57
57
  const adapter = new PermissiveAuthAdapter()
58
58
  const result = authFactory(adapter)
59
59
  expect(result).toBe(adapter)
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  // Interface
13
- export { AuthAdapter, AuthActions } from './AuthAdapter.js'
13
+ export { EntityAuthAdapter, AuthActions } from './EntityAuthAdapter.js'
14
14
 
15
15
  // Role Hierarchy (topological resolution)
16
16
  export { RoleHierarchy, createRoleHierarchy } from './RoleHierarchy.js'
package/src/index.js CHANGED
@@ -17,6 +17,9 @@ export { createQdadm } from './plugin.js'
17
17
  // Entity system
18
18
  export * from './entity/index.js'
19
19
 
20
+ // Session auth (user authentication)
21
+ export * from './auth/index.js'
22
+
20
23
  // Orchestrator
21
24
  export * from './orchestrator/index.js'
22
25
 
@@ -184,21 +184,21 @@ export class Kernel {
184
184
  this._createHookRegistry()
185
185
  this._createZoneRegistry()
186
186
  this._createDeferredRegistry()
187
- // 2. Register auth:ready deferred (if auth configured)
187
+ // 2. Create orchestrator early (modules need it for ctx.entity())
188
+ this._createOrchestrator()
189
+ // 3. Register auth:ready deferred (if auth configured)
188
190
  this._registerAuthDeferred()
189
- // 3. Initialize legacy modules (can use all services, registers routes)
191
+ // 4. Initialize legacy modules (can use all services, registers routes)
190
192
  this._initModules()
191
- // 3.5. Load new-style modules (moduleDefs) - synchronous for backward compat
193
+ // 4.5. Load new-style modules (moduleDefs) - synchronous for backward compat
192
194
  this._loadModulesSync()
193
- // 4. Create router (needs routes from modules)
195
+ // 5. Create router (needs routes from modules)
194
196
  this._createRouter()
195
- // 4.5. Setup auth guard (if authAdapter provided)
197
+ // 5.5. Setup auth guard (if authAdapter provided)
196
198
  this._setupAuthGuard()
197
- // 5. Setup auth:expired handler (needs router + authAdapter)
199
+ // 6. Setup auth:expired handler (needs router + authAdapter)
198
200
  this._setupAuthExpiredHandler()
199
- // 6. Create orchestrator and remaining components
200
- this._createOrchestrator()
201
- // 7. Wire modules that need orchestrator (phase 2)
201
+ // 7. Wire modules that need orchestrator (phase 2 - kernel:ready signal)
202
202
  this._wireModules()
203
203
  // 8. Create EventRouter (needs signals + orchestrator)
204
204
  this._createEventRouter()
@@ -229,21 +229,21 @@ export class Kernel {
229
229
  this._createHookRegistry()
230
230
  this._createZoneRegistry()
231
231
  this._createDeferredRegistry()
232
- // 2. Register auth:ready deferred (if auth configured)
232
+ // 2. Create orchestrator early (modules need it for ctx.entity())
233
+ this._createOrchestrator()
234
+ // 3. Register auth:ready deferred (if auth configured)
233
235
  this._registerAuthDeferred()
234
- // 3. Initialize legacy modules (can use all services, registers routes)
236
+ // 4. Initialize legacy modules (can use all services, registers routes)
235
237
  this._initModules()
236
- // 3.5. Load new-style modules (moduleDefs) - async version
238
+ // 4.5. Load new-style modules (moduleDefs) - async version
237
239
  await this._loadModules()
238
- // 4. Create router (needs routes from modules)
240
+ // 5. Create router (needs routes from modules)
239
241
  this._createRouter()
240
- // 4.5. Setup auth guard (if authAdapter provided)
242
+ // 5.5. Setup auth guard (if authAdapter provided)
241
243
  this._setupAuthGuard()
242
- // 5. Setup auth:expired handler (needs router + authAdapter)
244
+ // 6. Setup auth:expired handler (needs router + authAdapter)
243
245
  this._setupAuthExpiredHandler()
244
- // 6. Create orchestrator and remaining components
245
- this._createOrchestrator()
246
- // 7. Wire modules that need orchestrator (phase 2)
246
+ // 7. Wire modules that need orchestrator (phase 2 - kernel:ready signal)
247
247
  await this._wireModulesAsync()
248
248
  // 8. Create EventRouter (needs signals + orchestrator)
249
249
  this._createEventRouter()
@@ -222,6 +222,148 @@ export class KernelContext {
222
222
  return this
223
223
  }
224
224
 
225
+ /**
226
+ * Register standard CRUD routes with naming conventions
227
+ *
228
+ * Generates routes following qdadm conventions:
229
+ * - Entity 'books' → route prefix 'book'
230
+ * - List: /books → name 'book'
231
+ * - Create: /books/create → name 'book-create'
232
+ * - Edit: /books/:id/edit → name 'book-edit'
233
+ *
234
+ * Also registers route family and optional nav item.
235
+ *
236
+ * @param {string} entity - Entity name (plural, e.g., 'books', 'users')
237
+ * @param {object} pages - Page components (lazy imports)
238
+ * @param {Function} pages.list - List page: () => import('./pages/List.vue')
239
+ * @param {Function} [pages.form] - Single form for create+edit (recommended)
240
+ * @param {Function} [pages.create] - Separate create page (alternative to form)
241
+ * @param {Function} [pages.edit] - Separate edit page (alternative to form)
242
+ * @param {object} [options={}] - Additional options
243
+ * @param {object} [options.nav] - Navigation config
244
+ * @param {string} options.nav.section - Nav section (e.g., 'Library')
245
+ * @param {string} [options.nav.icon] - Icon class (e.g., 'pi pi-book')
246
+ * @param {string} [options.nav.label] - Display label (default: capitalized entity)
247
+ * @param {string} [options.routePrefix] - Override route prefix (default: singularized entity)
248
+ * @returns {this}
249
+ *
250
+ * @example
251
+ * // Minimal - list only (read-only entity)
252
+ * ctx.crud('countries', { list: () => import('./pages/CountriesList.vue') })
253
+ *
254
+ * @example
255
+ * // Single form pattern (recommended)
256
+ * ctx.crud('books', {
257
+ * list: () => import('./pages/BookList.vue'),
258
+ * form: () => import('./pages/BookForm.vue')
259
+ * }, {
260
+ * nav: { section: 'Library', icon: 'pi pi-book' }
261
+ * })
262
+ *
263
+ * @example
264
+ * // Separate create/edit pages
265
+ * ctx.crud('users', {
266
+ * list: () => import('./pages/UserList.vue'),
267
+ * create: () => import('./pages/UserCreate.vue'),
268
+ * edit: () => import('./pages/UserEdit.vue')
269
+ * }, {
270
+ * nav: { section: 'Admin', icon: 'pi pi-users', label: 'Users' }
271
+ * })
272
+ */
273
+ crud(entity, pages, options = {}) {
274
+ // Derive route prefix: 'books' → 'book', 'countries' → 'country'
275
+ const routePrefix = options.routePrefix || this._singularize(entity)
276
+
277
+ // Build routes array
278
+ const routes = []
279
+
280
+ // List route (always required)
281
+ if (pages.list) {
282
+ routes.push({
283
+ path: '',
284
+ name: routePrefix,
285
+ component: pages.list,
286
+ meta: { layout: 'list' }
287
+ })
288
+ }
289
+
290
+ // Form routes - single form or separate create/edit
291
+ if (pages.form) {
292
+ // Single form pattern (recommended)
293
+ routes.push({
294
+ path: 'create',
295
+ name: `${routePrefix}-create`,
296
+ component: pages.form
297
+ })
298
+ routes.push({
299
+ path: ':id/edit',
300
+ name: `${routePrefix}-edit`,
301
+ component: pages.form
302
+ })
303
+ } else {
304
+ // Separate create/edit pages
305
+ if (pages.create) {
306
+ routes.push({
307
+ path: 'create',
308
+ name: `${routePrefix}-create`,
309
+ component: pages.create
310
+ })
311
+ }
312
+ if (pages.edit) {
313
+ routes.push({
314
+ path: ':id/edit',
315
+ name: `${routePrefix}-edit`,
316
+ component: pages.edit
317
+ })
318
+ }
319
+ }
320
+
321
+ // Register routes with entity binding
322
+ this.routes(entity, routes, { entity })
323
+
324
+ // Register route family for active state detection
325
+ this.routeFamily(routePrefix, [`${routePrefix}-`])
326
+
327
+ // Register nav item if provided
328
+ if (options.nav) {
329
+ const label = options.nav.label || this._capitalize(entity)
330
+ this.navItem({
331
+ section: options.nav.section,
332
+ route: routePrefix,
333
+ icon: options.nav.icon,
334
+ label,
335
+ entity
336
+ })
337
+ }
338
+
339
+ return this
340
+ }
341
+
342
+ /**
343
+ * Singularize a plural word (simple English rules)
344
+ * @param {string} plural - Plural word
345
+ * @returns {string} Singular form
346
+ * @private
347
+ */
348
+ _singularize(plural) {
349
+ if (plural.endsWith('ies')) return plural.slice(0, -3) + 'y'
350
+ if (plural.endsWith('ses') || plural.endsWith('xes') || plural.endsWith('zes')) {
351
+ return plural.slice(0, -2)
352
+ }
353
+ if (plural.endsWith('s') && !plural.endsWith('ss')) return plural.slice(0, -1)
354
+ return plural
355
+ }
356
+
357
+ /**
358
+ * Capitalize first letter
359
+ * @param {string} str - String to capitalize
360
+ * @returns {string} Capitalized string
361
+ * @private
362
+ */
363
+ _capitalize(str) {
364
+ return str.charAt(0).toUpperCase() + str.slice(1)
365
+ }
366
+
225
367
  /**
226
368
  * Define a zone for extensible UI composition
227
369
  *
@@ -200,7 +200,27 @@ export class Orchestrator {
200
200
  }
201
201
  }
202
202
 
203
- throw new Error(`[Orchestrator] No manager for entity "${name}" and no factory provided`)
203
+ // Build helpful error message
204
+ const registered = this.getRegisteredNames()
205
+ const hint = registered.length > 0
206
+ ? `\nRegistered entities: ${registered.join(', ')}\n`
207
+ : '\nNo entities registered yet.\n'
208
+
209
+ const suggestion = `
210
+ Possible causes:
211
+ 1. Entity "${name}" not declared in any module (ctx.entity('${name}', {...}))
212
+ 2. Module not loaded (check moduleDefs in config/modules.js)
213
+ 3. Typo in entity name
214
+
215
+ Example fix in your module:
216
+ ctx.entity('${name}', {
217
+ name: '${name}',
218
+ labelField: 'name',
219
+ fields: { ... },
220
+ storage: yourStorage
221
+ })`
222
+
223
+ throw new Error(`[Orchestrator] No manager for entity "${name}"${hint}${suggestion}`)
204
224
  }
205
225
 
206
226
  /**
@@ -18,7 +18,13 @@ export function useOrchestrator() {
18
18
  const orchestrator = inject('qdadmOrchestrator')
19
19
 
20
20
  if (!orchestrator) {
21
- throw new Error('[qdadm] Orchestrator not provided. Make sure to use createQdadm() with entityFactory.')
21
+ throw new Error(
22
+ '[qdadm] Orchestrator not provided.\n' +
23
+ 'Possible causes:\n' +
24
+ '1. Kernel not initialized - ensure createKernel().createApp() is called before mounting\n' +
25
+ '2. Component used outside of qdadm app context\n' +
26
+ '3. Missing entityFactory in Kernel options'
27
+ )
22
28
  }
23
29
 
24
30
  /**