qdadm 0.30.0 → 0.32.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 +2 -1
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +1 -0
- package/src/composables/useAuth.js +44 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/debug/AuthCollector.js +340 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +403 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +174 -0
- package/src/debug/components/panels/EntitiesPanel.vue +712 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/auth/CompositeAuthAdapter.js +212 -0
- package/src/entity/auth/factory.js +207 -0
- package/src/entity/auth/factory.test.js +257 -0
- package/src/entity/auth/index.js +14 -0
- package/src/entity/storage/MockApiStorage.js +51 -2
- package/src/entity/storage/index.js +9 -2
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +468 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SignalBus.js +2 -7
- package/src/kernel/index.js +14 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- package/src/toast/useSignalToast.js +113 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for auth factory and CompositeAuthAdapter
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest'
|
|
5
|
+
import { AuthAdapter } from './AuthAdapter.js'
|
|
6
|
+
import { PermissiveAuthAdapter } from './PermissiveAdapter.js'
|
|
7
|
+
import { CompositeAuthAdapter } from './CompositeAuthAdapter.js'
|
|
8
|
+
import { authFactory, parseAuthPattern, authTypes } from './factory.js'
|
|
9
|
+
|
|
10
|
+
// Test adapter for custom types
|
|
11
|
+
class TestAuthAdapter extends AuthAdapter {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super()
|
|
14
|
+
this.options = options
|
|
15
|
+
}
|
|
16
|
+
canPerform() { return true }
|
|
17
|
+
canAccessRecord() { return true }
|
|
18
|
+
getCurrentUser() { return { id: 'test' } }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Adapter that tracks which entity was checked
|
|
22
|
+
class TrackingAdapter extends AuthAdapter {
|
|
23
|
+
constructor(name) {
|
|
24
|
+
super()
|
|
25
|
+
this.name = name
|
|
26
|
+
this.checkedEntities = []
|
|
27
|
+
}
|
|
28
|
+
canPerform(entity, action) {
|
|
29
|
+
this.checkedEntities.push({ entity, action })
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
canAccessRecord() { return true }
|
|
33
|
+
getCurrentUser() { return { name: this.name } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('parseAuthPattern', () => {
|
|
37
|
+
it('parses simple type', () => {
|
|
38
|
+
expect(parseAuthPattern('jwt')).toEqual({ type: 'jwt' })
|
|
39
|
+
expect(parseAuthPattern('permissive')).toEqual({ type: 'permissive' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('parses type with scope', () => {
|
|
43
|
+
expect(parseAuthPattern('jwt:internal')).toEqual({ type: 'jwt', scope: 'internal' })
|
|
44
|
+
expect(parseAuthPattern('apikey:external')).toEqual({ type: 'apikey', scope: 'external' })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns null for invalid input', () => {
|
|
48
|
+
expect(parseAuthPattern(null)).toBeNull()
|
|
49
|
+
expect(parseAuthPattern(123)).toBeNull()
|
|
50
|
+
expect(parseAuthPattern('')).toBeNull()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('authFactory', () => {
|
|
55
|
+
describe('instance passthrough', () => {
|
|
56
|
+
it('returns AuthAdapter instance as-is', () => {
|
|
57
|
+
const adapter = new PermissiveAuthAdapter()
|
|
58
|
+
const result = authFactory(adapter)
|
|
59
|
+
expect(result).toBe(adapter)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns duck-typed adapter as-is', () => {
|
|
63
|
+
const duckAdapter = {
|
|
64
|
+
canPerform: () => true,
|
|
65
|
+
canAccessRecord: () => true,
|
|
66
|
+
getCurrentUser: () => null
|
|
67
|
+
}
|
|
68
|
+
const result = authFactory(duckAdapter)
|
|
69
|
+
expect(result).toBe(duckAdapter)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('null/undefined', () => {
|
|
74
|
+
it('returns PermissiveAuthAdapter for null', () => {
|
|
75
|
+
const result = authFactory(null)
|
|
76
|
+
expect(result).toBeInstanceOf(PermissiveAuthAdapter)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns PermissiveAuthAdapter for undefined', () => {
|
|
80
|
+
const result = authFactory(undefined)
|
|
81
|
+
expect(result).toBeInstanceOf(PermissiveAuthAdapter)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('string patterns', () => {
|
|
86
|
+
it('resolves built-in types', () => {
|
|
87
|
+
const result = authFactory('permissive')
|
|
88
|
+
expect(result).toBeInstanceOf(PermissiveAuthAdapter)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('resolves custom types from context', () => {
|
|
92
|
+
const result = authFactory('test', {
|
|
93
|
+
authTypes: { test: TestAuthAdapter }
|
|
94
|
+
})
|
|
95
|
+
expect(result).toBeInstanceOf(TestAuthAdapter)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('throws for unknown type', () => {
|
|
99
|
+
expect(() => authFactory('unknown')).toThrow(/Unknown auth type/)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('config objects', () => {
|
|
104
|
+
it('resolves config with type', () => {
|
|
105
|
+
const result = authFactory(
|
|
106
|
+
{ type: 'test', foo: 'bar' },
|
|
107
|
+
{ authTypes: { test: TestAuthAdapter } }
|
|
108
|
+
)
|
|
109
|
+
expect(result).toBeInstanceOf(TestAuthAdapter)
|
|
110
|
+
expect(result.options.foo).toBe('bar')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('throws for config without type or default', () => {
|
|
114
|
+
expect(() => authFactory({ foo: 'bar' })).toThrow(/requires either "type" or "default"/)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('composite config', () => {
|
|
119
|
+
it('creates CompositeAuthAdapter from config', () => {
|
|
120
|
+
const defaultAdapter = new PermissiveAuthAdapter()
|
|
121
|
+
const result = authFactory(
|
|
122
|
+
{ default: defaultAdapter },
|
|
123
|
+
{ CompositeAuthAdapter }
|
|
124
|
+
)
|
|
125
|
+
expect(result).toBeInstanceOf(CompositeAuthAdapter)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('throws without CompositeAuthAdapter in context', () => {
|
|
129
|
+
const defaultAdapter = new PermissiveAuthAdapter()
|
|
130
|
+
expect(() => authFactory({ default: defaultAdapter })).toThrow(/CompositeAuthAdapter/)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('CompositeAuthAdapter', () => {
|
|
136
|
+
describe('routing', () => {
|
|
137
|
+
it('uses default adapter for unmatched entities', () => {
|
|
138
|
+
const defaultAdapter = new TrackingAdapter('default')
|
|
139
|
+
const composite = new CompositeAuthAdapter({ default: defaultAdapter })
|
|
140
|
+
|
|
141
|
+
composite.canPerform('books', 'read')
|
|
142
|
+
composite.canPerform('users', 'create')
|
|
143
|
+
|
|
144
|
+
expect(defaultAdapter.checkedEntities).toEqual([
|
|
145
|
+
{ entity: 'books', action: 'read' },
|
|
146
|
+
{ entity: 'users', action: 'create' }
|
|
147
|
+
])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('routes exact matches to mapped adapter', () => {
|
|
151
|
+
const defaultAdapter = new TrackingAdapter('default')
|
|
152
|
+
const productsAdapter = new TrackingAdapter('products')
|
|
153
|
+
|
|
154
|
+
const composite = new CompositeAuthAdapter({
|
|
155
|
+
default: defaultAdapter,
|
|
156
|
+
mapping: { products: productsAdapter }
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
composite.canPerform('books', 'read')
|
|
160
|
+
composite.canPerform('products', 'read')
|
|
161
|
+
|
|
162
|
+
expect(defaultAdapter.checkedEntities).toHaveLength(1)
|
|
163
|
+
expect(defaultAdapter.checkedEntities[0].entity).toBe('books')
|
|
164
|
+
|
|
165
|
+
expect(productsAdapter.checkedEntities).toHaveLength(1)
|
|
166
|
+
expect(productsAdapter.checkedEntities[0].entity).toBe('products')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('routes glob patterns to mapped adapter', () => {
|
|
170
|
+
const defaultAdapter = new TrackingAdapter('default')
|
|
171
|
+
const externalAdapter = new TrackingAdapter('external')
|
|
172
|
+
|
|
173
|
+
const composite = new CompositeAuthAdapter({
|
|
174
|
+
default: defaultAdapter,
|
|
175
|
+
mapping: { 'external-*': externalAdapter }
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
composite.canPerform('books', 'read')
|
|
179
|
+
composite.canPerform('external-products', 'read')
|
|
180
|
+
composite.canPerform('external-orders', 'list')
|
|
181
|
+
|
|
182
|
+
expect(defaultAdapter.checkedEntities).toHaveLength(1)
|
|
183
|
+
expect(externalAdapter.checkedEntities).toHaveLength(2)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('supports suffix glob patterns', () => {
|
|
187
|
+
const defaultAdapter = new TrackingAdapter('default')
|
|
188
|
+
const readonlyAdapter = new TrackingAdapter('readonly')
|
|
189
|
+
|
|
190
|
+
const composite = new CompositeAuthAdapter({
|
|
191
|
+
default: defaultAdapter,
|
|
192
|
+
mapping: { '*-readonly': readonlyAdapter }
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
composite.canPerform('products-readonly', 'read')
|
|
196
|
+
composite.canPerform('products', 'read')
|
|
197
|
+
|
|
198
|
+
expect(readonlyAdapter.checkedEntities).toHaveLength(1)
|
|
199
|
+
expect(defaultAdapter.checkedEntities).toHaveLength(1)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('getCurrentUser', () => {
|
|
204
|
+
it('returns user from default adapter', () => {
|
|
205
|
+
const defaultAdapter = new TrackingAdapter('admin')
|
|
206
|
+
const externalAdapter = new TrackingAdapter('service')
|
|
207
|
+
|
|
208
|
+
const composite = new CompositeAuthAdapter({
|
|
209
|
+
default: defaultAdapter,
|
|
210
|
+
mapping: { 'external-*': externalAdapter }
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(composite.getCurrentUser()).toEqual({ name: 'admin' })
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('factory integration', () => {
|
|
218
|
+
it('resolves adapters in mapping via factory', () => {
|
|
219
|
+
const defaultAdapter = new PermissiveAuthAdapter()
|
|
220
|
+
|
|
221
|
+
const composite = new CompositeAuthAdapter(
|
|
222
|
+
{
|
|
223
|
+
default: defaultAdapter,
|
|
224
|
+
mapping: {
|
|
225
|
+
products: { type: 'test' }
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{ authTypes: { test: TestAuthAdapter } }
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// Products should use TestAuthAdapter
|
|
232
|
+
expect(composite._getAdapter('products')).toBeInstanceOf(TestAuthAdapter)
|
|
233
|
+
// Others use default
|
|
234
|
+
expect(composite._getAdapter('books')).toBe(defaultAdapter)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe('getAdapterInfo', () => {
|
|
239
|
+
it('returns debug info about adapters', () => {
|
|
240
|
+
const composite = new CompositeAuthAdapter({
|
|
241
|
+
default: new PermissiveAuthAdapter(),
|
|
242
|
+
mapping: {
|
|
243
|
+
products: new TestAuthAdapter(),
|
|
244
|
+
'external-*': new TrackingAdapter('ext')
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const info = composite.getAdapterInfo()
|
|
249
|
+
|
|
250
|
+
expect(info.default).toBe('PermissiveAuthAdapter')
|
|
251
|
+
expect(info.exactMatches.products).toBe('TestAuthAdapter')
|
|
252
|
+
expect(info.patterns).toHaveLength(1)
|
|
253
|
+
expect(info.patterns[0].pattern).toBe('external-*')
|
|
254
|
+
expect(info.patterns[0].adapter).toBe('TrackingAdapter')
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
})
|
package/src/entity/auth/index.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* AuthAdapter interface and implementations for scope/silo permission checks.
|
|
5
5
|
* Includes Symfony-inspired role hierarchy and security checker.
|
|
6
|
+
*
|
|
7
|
+
* Multi-source auth:
|
|
8
|
+
* - CompositeAuthAdapter routes to different adapters based on entity patterns
|
|
9
|
+
* - authFactory normalizes config (instance, string, or object)
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
// Interface
|
|
@@ -16,3 +20,13 @@ export { SecurityChecker, createSecurityChecker } from './SecurityChecker.js'
|
|
|
16
20
|
|
|
17
21
|
// Implementations
|
|
18
22
|
export { PermissiveAuthAdapter, createPermissiveAdapter } from './PermissiveAdapter.js'
|
|
23
|
+
export { CompositeAuthAdapter, createCompositeAuthAdapter } from './CompositeAuthAdapter.js'
|
|
24
|
+
|
|
25
|
+
// Factory/Resolver pattern (like storage/factory.js)
|
|
26
|
+
export {
|
|
27
|
+
authFactory,
|
|
28
|
+
createAuthFactory,
|
|
29
|
+
authTypes,
|
|
30
|
+
parseAuthPattern,
|
|
31
|
+
defaultAuthResolver
|
|
32
|
+
} from './factory.js'
|
|
@@ -29,7 +29,23 @@ export class MockApiStorage extends IStorage {
|
|
|
29
29
|
* - supportsTotal: true - Returns accurate total from in-memory data
|
|
30
30
|
* - supportsFilters: true - Filters in-memory via list() params
|
|
31
31
|
* - supportsPagination: true - Paginates in-memory
|
|
32
|
-
* - supportsCaching: false -
|
|
32
|
+
* - supportsCaching: false - Suggests EntityManager cache is not useful (see below)
|
|
33
|
+
*
|
|
34
|
+
* WHY supportsCaching = false?
|
|
35
|
+
* MockApiStorage is already in-memory, so the primary benefit of caching (avoiding
|
|
36
|
+
* network calls) doesn't apply. However, EntityManager cache CAN still be enabled
|
|
37
|
+
* by subclassing and setting supportsCaching: true. This is useful when you need:
|
|
38
|
+
* - Parent field resolution for searchFields (e.g., 'book.title' in loans)
|
|
39
|
+
* - Warmup at boot for consistent initial state
|
|
40
|
+
* - Local filtering without re-calling list()
|
|
41
|
+
*
|
|
42
|
+
* To enable EntityManager caching on MockApiStorage:
|
|
43
|
+
* ```js
|
|
44
|
+
* class CacheableMockStorage extends MockApiStorage {
|
|
45
|
+
* static capabilities = { ...MockApiStorage.capabilities, supportsCaching: true }
|
|
46
|
+
* get supportsCaching() { return true }
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
33
49
|
*
|
|
34
50
|
* @type {import('./index.js').StorageCapabilities}
|
|
35
51
|
*/
|
|
@@ -49,13 +65,26 @@ export class MockApiStorage extends IStorage {
|
|
|
49
65
|
return MockApiStorage.capabilities.supportsCaching
|
|
50
66
|
}
|
|
51
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Instance capabilities getter.
|
|
70
|
+
* Merges static capabilities with instance-specific ones like requiresAuth.
|
|
71
|
+
* @returns {object}
|
|
72
|
+
*/
|
|
73
|
+
get capabilities() {
|
|
74
|
+
return {
|
|
75
|
+
...MockApiStorage.capabilities,
|
|
76
|
+
requiresAuth: !!this._authCheck
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
constructor(options = {}) {
|
|
53
81
|
super()
|
|
54
82
|
const {
|
|
55
83
|
entityName,
|
|
56
84
|
idField = 'id',
|
|
57
85
|
generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
|
|
58
|
-
initialData = null
|
|
86
|
+
initialData = null,
|
|
87
|
+
authCheck = null // Optional: () => boolean - throws 401 if returns false
|
|
59
88
|
} = options
|
|
60
89
|
|
|
61
90
|
if (!entityName) {
|
|
@@ -67,11 +96,24 @@ export class MockApiStorage extends IStorage {
|
|
|
67
96
|
this.generateId = generateId
|
|
68
97
|
this._storageKey = `mockapi_${entityName}_data`
|
|
69
98
|
this._data = new Map()
|
|
99
|
+
this._authCheck = authCheck
|
|
70
100
|
|
|
71
101
|
// Load from localStorage or seed with initialData
|
|
72
102
|
this._loadFromStorage(initialData)
|
|
73
103
|
}
|
|
74
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Check authentication if authCheck is configured
|
|
107
|
+
* @throws {Error} 401 error if not authenticated
|
|
108
|
+
*/
|
|
109
|
+
_checkAuth() {
|
|
110
|
+
if (this._authCheck && !this._authCheck()) {
|
|
111
|
+
const error = new Error(`Unauthorized: Authentication required to access ${this.entityName}`)
|
|
112
|
+
error.status = 401
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
75
117
|
/**
|
|
76
118
|
* Load data from localStorage, seed with initialData if empty
|
|
77
119
|
* @param {Array|null} initialData - Data to seed if storage is empty
|
|
@@ -142,6 +184,7 @@ export class MockApiStorage extends IStorage {
|
|
|
142
184
|
* @returns {Promise<{ items: Array, total: number }>}
|
|
143
185
|
*/
|
|
144
186
|
async list(params = {}) {
|
|
187
|
+
this._checkAuth()
|
|
145
188
|
const { page = 1, page_size = 20, sort_by, sort_order = 'asc', filters = {}, search } = params
|
|
146
189
|
|
|
147
190
|
let items = this._getAll()
|
|
@@ -197,6 +240,7 @@ export class MockApiStorage extends IStorage {
|
|
|
197
240
|
* @returns {Promise<object>}
|
|
198
241
|
*/
|
|
199
242
|
async get(id) {
|
|
243
|
+
this._checkAuth()
|
|
200
244
|
const item = this._data.get(String(id))
|
|
201
245
|
if (!item) {
|
|
202
246
|
const error = new Error(`Entity not found: ${id}`)
|
|
@@ -212,6 +256,7 @@ export class MockApiStorage extends IStorage {
|
|
|
212
256
|
* @returns {Promise<Array<object>>}
|
|
213
257
|
*/
|
|
214
258
|
async getMany(ids) {
|
|
259
|
+
this._checkAuth()
|
|
215
260
|
if (!ids || ids.length === 0) return []
|
|
216
261
|
const results = []
|
|
217
262
|
for (const id of ids) {
|
|
@@ -266,6 +311,7 @@ export class MockApiStorage extends IStorage {
|
|
|
266
311
|
* @returns {Promise<object>}
|
|
267
312
|
*/
|
|
268
313
|
async create(data) {
|
|
314
|
+
this._checkAuth()
|
|
269
315
|
const id = data[this.idField] || this.generateId()
|
|
270
316
|
const newItem = {
|
|
271
317
|
...data,
|
|
@@ -284,6 +330,7 @@ export class MockApiStorage extends IStorage {
|
|
|
284
330
|
* @returns {Promise<object>}
|
|
285
331
|
*/
|
|
286
332
|
async update(id, data) {
|
|
333
|
+
this._checkAuth()
|
|
287
334
|
if (!this._data.has(String(id))) {
|
|
288
335
|
const error = new Error(`Entity not found: ${id}`)
|
|
289
336
|
error.status = 404
|
|
@@ -306,6 +353,7 @@ export class MockApiStorage extends IStorage {
|
|
|
306
353
|
* @returns {Promise<object>}
|
|
307
354
|
*/
|
|
308
355
|
async patch(id, data) {
|
|
356
|
+
this._checkAuth()
|
|
309
357
|
const existing = this._data.get(String(id))
|
|
310
358
|
if (!existing) {
|
|
311
359
|
const error = new Error(`Entity not found: ${id}`)
|
|
@@ -328,6 +376,7 @@ export class MockApiStorage extends IStorage {
|
|
|
328
376
|
* @returns {Promise<void>}
|
|
329
377
|
*/
|
|
330
378
|
async delete(id) {
|
|
379
|
+
this._checkAuth()
|
|
331
380
|
if (!this._data.has(String(id))) {
|
|
332
381
|
const error = new Error(`Entity not found: ${id}`)
|
|
333
382
|
error.status = 404
|
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
* @property {boolean} [supportsTotal=false] - Storage `list()` returns `{ items, total }` with accurate total count
|
|
20
20
|
* @property {boolean} [supportsFilters=false] - Storage `list()` accepts `filters` param and handles filtering
|
|
21
21
|
* @property {boolean} [supportsPagination=false] - Storage `list()` accepts `page`/`page_size` params
|
|
22
|
-
* @property {boolean} [supportsCaching=false] -
|
|
22
|
+
* @property {boolean} [supportsCaching=false] - Hint to EntityManager: should it cache this storage's results?
|
|
23
|
+
* This is a RECOMMENDATION, not a capability. The storage suggests whether EntityManager caching is useful:
|
|
24
|
+
* - `true`: Cache is beneficial (remote APIs, slow storages) - enables local filtering, parent field
|
|
25
|
+
* resolution, warmup at boot
|
|
26
|
+
* - `false`: Cache adds no value (already in-memory storages like MockApiStorage) - but still technically
|
|
27
|
+
* possible if overridden. Set to `false` when storage is already fast and in-memory.
|
|
28
|
+
* Use `false` for: in-memory storages, real-time data sources, storages with their own cache layer.
|
|
29
|
+
* Use `true` for: REST APIs, database backends, any storage where avoiding repeat fetches helps.
|
|
23
30
|
* @property {string[]} [searchFields] - Fields to search when filtering locally. Supports own fields ('title')
|
|
24
31
|
* and parent entity fields ('book.title') via EntityManager.parents config. When undefined, all string
|
|
25
32
|
* fields are searched (default behavior). When defined, only listed fields are searched.
|
|
@@ -40,7 +47,7 @@
|
|
|
40
47
|
* supportsTotal: true,
|
|
41
48
|
* supportsFilters: true,
|
|
42
49
|
* supportsPagination: true,
|
|
43
|
-
* supportsCaching: true //
|
|
50
|
+
* supportsCaching: true // false = "not useful" (in-memory), true = "beneficial" (remote API)
|
|
44
51
|
* }
|
|
45
52
|
*
|
|
46
53
|
* // Backward-compat instance getter (optional, for smooth migration)
|
package/src/index.js
CHANGED
|
@@ -47,5 +47,12 @@ export * from './query/index.js'
|
|
|
47
47
|
// Utils
|
|
48
48
|
export * from './utils/index.js'
|
|
49
49
|
|
|
50
|
+
// Toast (signal-based notifications)
|
|
51
|
+
export * from './toast/index.js'
|
|
52
|
+
|
|
53
|
+
// Debug tools are NOT exported here to enable tree-shaking.
|
|
54
|
+
// Import from 'qdadm/debug' separately when needed:
|
|
55
|
+
// import { debugBar, DebugModule } from 'qdadm/debug'
|
|
56
|
+
|
|
50
57
|
// Assets
|
|
51
58
|
export { default as qdadmLogo } from './assets/logo.svg'
|