qdadm 0.32.0 → 0.35.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.
@@ -0,0 +1,335 @@
1
+ <script setup>
2
+ /**
3
+ * SignalsPanel - Debug panel for signals with pattern filter
4
+ *
5
+ * Supports QuarKernel wildcard patterns:
6
+ * - auth:** → all auth signals (auth:login, auth:impersonate:start)
7
+ * - cache:** → all cache signals
8
+ * - *:created → all creation signals
9
+ * - ** → all signals (default)
10
+ */
11
+ import { ref, computed, watch } from 'vue'
12
+ import InputText from 'primevue/inputtext'
13
+ import ObjectTree from '../ObjectTree.vue'
14
+
15
+ const props = defineProps({
16
+ collector: { type: Object, required: true },
17
+ entries: { type: Array, required: true }
18
+ })
19
+
20
+ // Filter state - persisted in localStorage
21
+ const STORAGE_KEY = 'qdadm-signals-filter'
22
+ const STORAGE_KEY_MAX = 'qdadm-signals-max'
23
+ const filterPattern = ref(localStorage.getItem(STORAGE_KEY) || '')
24
+ const maxSignals = ref(parseInt(localStorage.getItem(STORAGE_KEY_MAX)) || 50)
25
+
26
+ watch(filterPattern, (val) => {
27
+ localStorage.setItem(STORAGE_KEY, val)
28
+ })
29
+
30
+ watch(maxSignals, (val) => {
31
+ localStorage.setItem(STORAGE_KEY_MAX, String(val))
32
+ })
33
+
34
+ // Convert wildcard pattern to regex
35
+ function wildcardToRegex(pattern) {
36
+ if (!pattern || pattern === '**') return null // No filter
37
+
38
+ // Escape regex special chars except * and :
39
+ let regex = pattern
40
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
41
+ // ** matches anything (including colons)
42
+ .replace(/\*\*/g, '.*')
43
+ // * matches anything except colon
44
+ .replace(/\*/g, '[^:]*')
45
+
46
+ return new RegExp(`^${regex}$`)
47
+ }
48
+
49
+ const filterRegex = computed(() => wildcardToRegex(filterPattern.value.trim()))
50
+
51
+ // Apply filter, max limit, and reverse for top-down (newest first)
52
+ const filteredEntries = computed(() => {
53
+ let result = props.entries
54
+ if (filterRegex.value) {
55
+ result = result.filter(e => filterRegex.value.test(e.name))
56
+ }
57
+ // Apply max limit (slice from end to keep newest)
58
+ if (maxSignals.value > 0 && result.length > maxSignals.value) {
59
+ result = result.slice(-maxSignals.value)
60
+ }
61
+ // Reverse for top-down display (newest first)
62
+ return [...result].reverse()
63
+ })
64
+
65
+ const filterStats = computed(() => {
66
+ const total = props.entries.length
67
+ const shown = filteredEntries.value.length
68
+ return total !== shown ? `${shown}/${total}` : `${total}`
69
+ })
70
+
71
+ // Preset filters
72
+ const presets = [
73
+ { label: 'All', pattern: '' },
74
+ { label: 'data', pattern: 'entity:data-invalidate' },
75
+ { label: 'datalayer', pattern: 'entity:datalayer-invalidate' },
76
+ { label: 'auth', pattern: 'auth:**' },
77
+ { label: 'entity', pattern: 'entity:**' },
78
+ { label: 'toast', pattern: 'toast:**' }
79
+ ]
80
+
81
+ function applyPreset(pattern) {
82
+ filterPattern.value = pattern
83
+ }
84
+
85
+ function formatTime(ts) {
86
+ const d = new Date(ts)
87
+ return d.toLocaleTimeString('en-US', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0')
88
+ }
89
+
90
+ // Extract domain from signal name (first segment)
91
+ function getDomain(name) {
92
+ return name.split(':')[0]
93
+ }
94
+
95
+ const domainColors = {
96
+ auth: '#10b981',
97
+ cache: '#f59e0b',
98
+ entity: '#3b82f6',
99
+ toast: '#8b5cf6',
100
+ route: '#06b6d4',
101
+ error: '#ef4444'
102
+ }
103
+
104
+ function getDomainColor(name) {
105
+ const domain = getDomain(name)
106
+ return domainColors[domain] || '#6b7280'
107
+ }
108
+ </script>
109
+
110
+ <template>
111
+ <div class="signals-panel">
112
+ <!-- Filter bar -->
113
+ <div class="signals-filter">
114
+ <div class="signals-filter-input">
115
+ <i class="pi pi-filter" />
116
+ <InputText
117
+ v-model="filterPattern"
118
+ placeholder="Filter: auth:** cache:** *:created"
119
+ class="filter-input"
120
+ />
121
+ <span class="signals-count">{{ filterStats }}</span>
122
+ <span class="signals-max-label">max</span>
123
+ <input
124
+ v-model.number="maxSignals"
125
+ type="number"
126
+ min="10"
127
+ max="500"
128
+ class="max-input"
129
+ />
130
+ </div>
131
+ <div class="signals-presets">
132
+ <button
133
+ v-for="p in presets"
134
+ :key="p.pattern"
135
+ class="preset-btn"
136
+ :class="{ 'preset-active': filterPattern === p.pattern }"
137
+ @click="applyPreset(p.pattern)"
138
+ >
139
+ {{ p.label }}
140
+ </button>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Entries list -->
145
+ <div class="signals-list">
146
+ <div v-if="filteredEntries.length === 0" class="signals-empty">
147
+ <i class="pi pi-inbox" />
148
+ <span>{{ entries.length === 0 ? 'No signals' : 'No matching signals' }}</span>
149
+ </div>
150
+
151
+ <div
152
+ v-for="(entry, idx) in filteredEntries"
153
+ :key="idx"
154
+ class="signal-entry"
155
+ :class="{ 'signal-new': entry._isNew }"
156
+ >
157
+ <div class="signal-header">
158
+ <span v-if="entry._isNew" class="signal-new-dot" title="New (unseen)" />
159
+ <span class="signal-time">{{ formatTime(entry.timestamp) }}</span>
160
+ <span class="signal-name" :style="{ color: getDomainColor(entry.name) }">
161
+ {{ entry.name }}
162
+ </span>
163
+ </div>
164
+ <div v-if="entry.data && Object.keys(entry.data).length > 0" class="signal-data">
165
+ <ObjectTree :data="entry.data" :expanded="false" />
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </template>
171
+
172
+ <style scoped>
173
+ .signals-panel {
174
+ display: flex;
175
+ flex-direction: column;
176
+ height: 100%;
177
+ }
178
+
179
+ .signals-filter {
180
+ padding: 8px 12px;
181
+ background: #27272a;
182
+ border-bottom: 1px solid #3f3f46;
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 6px;
186
+ }
187
+
188
+ .signals-filter-input {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 8px;
192
+ }
193
+
194
+ .signals-filter-input .pi {
195
+ color: #71717a;
196
+ font-size: 12px;
197
+ }
198
+
199
+ .filter-input {
200
+ flex: 1;
201
+ font-size: 12px;
202
+ padding: 4px 8px;
203
+ background: #18181b;
204
+ border: 1px solid #3f3f46;
205
+ border-radius: 4px;
206
+ color: #f4f4f5;
207
+ }
208
+
209
+ .filter-input:focus {
210
+ border-color: #8b5cf6;
211
+ outline: none;
212
+ }
213
+
214
+ .signals-count {
215
+ font-size: 11px;
216
+ color: #71717a;
217
+ min-width: 40px;
218
+ text-align: right;
219
+ }
220
+
221
+ .signals-max-label {
222
+ font-size: 10px;
223
+ color: #52525b;
224
+ margin-left: 8px;
225
+ }
226
+
227
+ .max-input {
228
+ width: 50px;
229
+ font-size: 11px;
230
+ padding: 2px 4px;
231
+ background: #18181b;
232
+ border: 1px solid #3f3f46;
233
+ border-radius: 3px;
234
+ color: #a1a1aa;
235
+ text-align: center;
236
+ }
237
+
238
+ .max-input:focus {
239
+ border-color: #8b5cf6;
240
+ outline: none;
241
+ color: #f4f4f5;
242
+ }
243
+
244
+ .signals-presets {
245
+ display: flex;
246
+ gap: 4px;
247
+ flex-wrap: wrap;
248
+ }
249
+
250
+ .preset-btn {
251
+ padding: 2px 8px;
252
+ font-size: 10px;
253
+ background: #3f3f46;
254
+ border: none;
255
+ border-radius: 3px;
256
+ color: #a1a1aa;
257
+ cursor: pointer;
258
+ }
259
+
260
+ .preset-btn:hover {
261
+ background: #52525b;
262
+ color: #f4f4f5;
263
+ }
264
+
265
+ .preset-active {
266
+ background: #8b5cf6;
267
+ color: white;
268
+ }
269
+
270
+ .signals-list {
271
+ flex: 1;
272
+ overflow-y: auto;
273
+ padding: 4px;
274
+ }
275
+
276
+ .signals-empty {
277
+ display: flex;
278
+ flex-direction: column;
279
+ align-items: center;
280
+ justify-content: center;
281
+ height: 100%;
282
+ color: #71717a;
283
+ gap: 8px;
284
+ }
285
+
286
+ .signal-entry {
287
+ padding: 6px 8px;
288
+ margin-bottom: 2px;
289
+ background: #27272a;
290
+ border-radius: 4px;
291
+ font-size: 12px;
292
+ }
293
+
294
+ .signal-entry:hover {
295
+ background: #3f3f46;
296
+ }
297
+
298
+ .signal-entry.signal-new {
299
+ border-left: 2px solid #f59e0b;
300
+ padding-left: 6px;
301
+ }
302
+
303
+ .signal-new-dot {
304
+ display: inline-block;
305
+ width: 6px;
306
+ height: 6px;
307
+ background: #f59e0b;
308
+ border-radius: 50%;
309
+ margin-right: 4px;
310
+ flex-shrink: 0;
311
+ }
312
+
313
+ .signal-header {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 8px;
317
+ }
318
+
319
+ .signal-time {
320
+ font-size: 10px;
321
+ color: #71717a;
322
+ font-family: monospace;
323
+ }
324
+
325
+ .signal-name {
326
+ font-weight: 500;
327
+ font-family: monospace;
328
+ }
329
+
330
+ .signal-data {
331
+ margin-top: 4px;
332
+ padding-left: 12px;
333
+ font-size: 11px;
334
+ }
335
+ </style>
@@ -6,3 +6,4 @@ export { default as AuthPanel } from './AuthPanel.vue'
6
6
  export { default as EntitiesPanel } from './EntitiesPanel.vue'
7
7
  export { default as ToastsPanel } from './ToastsPanel.vue'
8
8
  export { default as EntriesPanel } from './EntriesPanel.vue'
9
+ export { default as SignalsPanel } from './SignalsPanel.vue'
@@ -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
  /**
@@ -59,6 +59,7 @@ export class EntityManager {
59
59
  localFilterThreshold = null, // Items threshold to switch to local filtering (null = use default)
60
60
  readOnly = false, // If true, canCreate/canUpdate/canDelete return false
61
61
  warmup = true, // If true, cache is preloaded at boot via DeferredRegistry
62
+ authSensitive = false, // If true, auto-invalidate datalayer on auth events
62
63
  // Scope control
63
64
  scopeWhitelist = null, // Array of scopes/modules that can bypass restrictions
64
65
  // Relations
@@ -85,6 +86,7 @@ export class EntityManager {
85
86
  this.localFilterThreshold = localFilterThreshold
86
87
  this._readOnly = readOnly
87
88
  this._warmup = warmup
89
+ this._authSensitive = authSensitive
88
90
 
89
91
  // Scope control
90
92
  this._scopeWhitelist = scopeWhitelist
@@ -174,6 +176,25 @@ export class EntityManager {
174
176
  this._signals.emitEntity(this.name, action, data)
175
177
  }
176
178
 
179
+ /**
180
+ * Emit entity:data-invalidate signal for client cache invalidation
181
+ *
182
+ * This is a unified signal for clients to know when entity data has changed.
183
+ * Clients can listen to `entity:data-invalidate` to refresh their views.
184
+ *
185
+ * @param {string} action - 'created', 'updated', 'deleted'
186
+ * @param {string|number} id - The affected record ID
187
+ * @private
188
+ */
189
+ _emitDataInvalidate(action, id) {
190
+ if (!this._signals) return
191
+ this._signals.emit('entity:data-invalidate', {
192
+ entity: this.name,
193
+ action,
194
+ id
195
+ })
196
+ }
197
+
177
198
  // ============ LIFECYCLE HOOKS ============
178
199
 
179
200
  /**
@@ -860,6 +881,7 @@ export class EntityManager {
860
881
  manager: this.name,
861
882
  id: result?.[this.idField]
862
883
  })
884
+ this._emitDataInvalidate('created', result?.[this.idField])
863
885
  return result
864
886
  }
865
887
  throw new Error(`[EntityManager:${this.name}] create() not implemented`)
@@ -896,6 +918,7 @@ export class EntityManager {
896
918
  manager: this.name,
897
919
  id
898
920
  })
921
+ this._emitDataInvalidate('updated', id)
899
922
  return result
900
923
  }
901
924
  throw new Error(`[EntityManager:${this.name}] update() not implemented`)
@@ -932,6 +955,7 @@ export class EntityManager {
932
955
  manager: this.name,
933
956
  id
934
957
  })
958
+ this._emitDataInvalidate('updated', id)
935
959
  return result
936
960
  }
937
961
  throw new Error(`[EntityManager:${this.name}] patch() not implemented`)
@@ -959,6 +983,7 @@ export class EntityManager {
959
983
  manager: this.name,
960
984
  id
961
985
  })
986
+ this._emitDataInvalidate('deleted', id)
962
987
  return result
963
988
  }
964
989
  throw new Error(`[EntityManager:${this.name}] delete() not implemented`)
@@ -1143,11 +1168,12 @@ export class EntityManager {
1143
1168
 
1144
1169
  const cleanups = []
1145
1170
 
1146
- // Listen for parent cache invalidation (if parents defined)
1171
+ // Listen for parent data changes (if parents defined)
1172
+ // When a parent entity's data changes, clear search cache to re-resolve parent fields
1147
1173
  if (this._parents && Object.keys(this._parents).length > 0) {
1148
1174
  const parentEntities = Object.values(this._parents).map(p => p.entity)
1149
1175
  cleanups.push(
1150
- this._signals.on('cache:entity:invalidated', ({ entity }) => {
1176
+ this._signals.on('entity:data-invalidate', ({ entity }) => {
1151
1177
  if (parentEntities.includes(entity)) {
1152
1178
  this._clearSearchCache()
1153
1179
  }
@@ -1155,12 +1181,28 @@ export class EntityManager {
1155
1181
  )
1156
1182
  }
1157
1183
 
1158
- // Listen for targeted cache invalidation (routed by EventRouter)
1159
- // EntityManager only listens to its own signal, staying simple.
1160
- // EventRouter transforms high-level events (auth:impersonate) into targeted signals.
1184
+ // Design choice: EntityManager-centric architecture
1185
+ // Storages stay simple (pure data access), EntityManager owns invalidation logic.
1186
+ //
1187
+ // Signal: entity:datalayer-invalidate { entity, actuator? }
1188
+ // - entity: target entity name, '*' for all, or null/undefined for all
1189
+ // - actuator: source of the signal ('auth' for auth events, undefined for manual)
1190
+ //
1191
+ // Routing is handled by EventRouter (configured at Kernel level):
1192
+ // 'auth:**' → { signal: 'entity:datalayer-invalidate', transform: () => ({ actuator: 'auth' }) }
1193
+ //
1194
+ // Only authSensitive entities react to actuator='auth' signals.
1195
+ // All entities react to manual signals (no actuator).
1196
+
1161
1197
  cleanups.push(
1162
- this._signals.on(`cache:entity:invalidate:${this.name}`, () => {
1163
- this.invalidateCache()
1198
+ this._signals.on('entity:datalayer-invalidate', ({ entity, actuator } = {}) => {
1199
+ // Auth-triggered: only react if authSensitive
1200
+ if (actuator === 'auth' && !this._authSensitive) return
1201
+
1202
+ // Check entity match (global or targeted)
1203
+ if (!entity || entity === '*' || entity === this.name) {
1204
+ this.invalidateDataLayer()
1205
+ }
1164
1206
  })
1165
1207
  )
1166
1208
 
@@ -1216,24 +1258,34 @@ export class EntityManager {
1216
1258
  }
1217
1259
 
1218
1260
  /**
1219
- * Invalidate the cache (call after create/update/delete)
1261
+ * Invalidate the cache, forcing next list() to refetch
1220
1262
  *
1221
- * Emits cache:entity:invalidated signal only when cache was previously valid
1222
- * to avoid duplicate signals on repeated invalidation calls.
1263
+ * Note: entity:data-invalidate signal is emitted by CRUD methods,
1264
+ * not here (to include action and id context).
1223
1265
  */
1224
1266
  invalidateCache() {
1225
- const wasValid = this._cache.valid
1226
-
1227
1267
  this._cache.valid = false
1228
1268
  this._cache.items = []
1229
1269
  this._cache.total = 0
1230
1270
  this._cache.loadedAt = null
1231
1271
  this._cacheLoading = null
1272
+ }
1273
+
1274
+ /**
1275
+ * Invalidate the entire data layer (cache + storage state)
1276
+ *
1277
+ * Called when external context changes (auth, impersonation, etc.)
1278
+ * that may affect what data the storage returns.
1279
+ *
1280
+ * - Clears EntityManager cache
1281
+ * - Calls storage.reset() if available (for storages with internal state)
1282
+ */
1283
+ invalidateDataLayer() {
1284
+ this.invalidateCache()
1232
1285
 
1233
- // Emit cache invalidation signal only when cache was actually valid
1234
- // This prevents noise on repeated invalidation calls
1235
- if (wasValid && this._signals) {
1236
- this._signals.emit('cache:entity:invalidated', { entity: this.name })
1286
+ // Reset storage if it supports it (e.g., clear auth tokens, cached responses)
1287
+ if (typeof this.storage?.reset === 'function') {
1288
+ this.storage.reset()
1237
1289
  }
1238
1290
  }
1239
1291
 
@@ -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
  *