qdadm 0.13.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +270 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/package.json +48 -0
  5. package/src/assets/logo.svg +6 -0
  6. package/src/components/BoolCell.vue +28 -0
  7. package/src/components/dialogs/BulkStatusDialog.vue +43 -0
  8. package/src/components/dialogs/MultiStepDialog.vue +321 -0
  9. package/src/components/dialogs/SimpleDialog.vue +108 -0
  10. package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
  11. package/src/components/display/CardsGrid.vue +155 -0
  12. package/src/components/display/CopyableId.vue +92 -0
  13. package/src/components/display/EmptyState.vue +114 -0
  14. package/src/components/display/IntensityBar.vue +171 -0
  15. package/src/components/display/RichCardsGrid.vue +220 -0
  16. package/src/components/editors/JsonEditorFoldable.vue +467 -0
  17. package/src/components/editors/JsonStructuredField.vue +218 -0
  18. package/src/components/editors/JsonViewer.vue +91 -0
  19. package/src/components/editors/KeyValueEditor.vue +314 -0
  20. package/src/components/editors/LanguageEditor.vue +245 -0
  21. package/src/components/editors/ScopeEditor.vue +341 -0
  22. package/src/components/editors/VanillaJsonEditor.vue +185 -0
  23. package/src/components/forms/FormActions.vue +104 -0
  24. package/src/components/forms/FormField.vue +64 -0
  25. package/src/components/forms/FormTab.vue +217 -0
  26. package/src/components/forms/FormTabs.vue +108 -0
  27. package/src/components/index.js +44 -0
  28. package/src/components/layout/AppLayout.vue +430 -0
  29. package/src/components/layout/Breadcrumb.vue +106 -0
  30. package/src/components/layout/PageHeader.vue +75 -0
  31. package/src/components/layout/PageLayout.vue +93 -0
  32. package/src/components/lists/ActionButtons.vue +41 -0
  33. package/src/components/lists/ActionColumn.vue +37 -0
  34. package/src/components/lists/FilterBar.vue +53 -0
  35. package/src/components/lists/ListPage.vue +319 -0
  36. package/src/composables/index.js +19 -0
  37. package/src/composables/useApp.js +43 -0
  38. package/src/composables/useAuth.js +49 -0
  39. package/src/composables/useBareForm.js +143 -0
  40. package/src/composables/useBreadcrumb.js +221 -0
  41. package/src/composables/useDirtyState.js +103 -0
  42. package/src/composables/useEntityTitle.js +121 -0
  43. package/src/composables/useForm.js +254 -0
  44. package/src/composables/useGuardStore.js +37 -0
  45. package/src/composables/useJsonSyntax.js +101 -0
  46. package/src/composables/useListPageBuilder.js +1176 -0
  47. package/src/composables/useNavigation.js +89 -0
  48. package/src/composables/usePageBuilder.js +334 -0
  49. package/src/composables/useStatus.js +146 -0
  50. package/src/composables/useSubEditor.js +165 -0
  51. package/src/composables/useTabSync.js +110 -0
  52. package/src/composables/useUnsavedChangesGuard.js +122 -0
  53. package/src/entity/EntityManager.js +540 -0
  54. package/src/entity/index.js +11 -0
  55. package/src/entity/storage/ApiStorage.js +146 -0
  56. package/src/entity/storage/LocalStorage.js +220 -0
  57. package/src/entity/storage/MemoryStorage.js +201 -0
  58. package/src/entity/storage/index.js +10 -0
  59. package/src/index.js +29 -0
  60. package/src/kernel/Kernel.js +234 -0
  61. package/src/kernel/index.js +7 -0
  62. package/src/module/index.js +16 -0
  63. package/src/module/moduleRegistry.js +222 -0
  64. package/src/orchestrator/Orchestrator.js +141 -0
  65. package/src/orchestrator/index.js +8 -0
  66. package/src/orchestrator/useOrchestrator.js +61 -0
  67. package/src/plugin.js +142 -0
  68. package/src/styles/_alerts.css +48 -0
  69. package/src/styles/_code.css +33 -0
  70. package/src/styles/_dialogs.css +17 -0
  71. package/src/styles/_markdown.css +82 -0
  72. package/src/styles/_show-pages.css +84 -0
  73. package/src/styles/index.css +16 -0
  74. package/src/styles/main.css +845 -0
  75. package/src/styles/theme/components.css +286 -0
  76. package/src/styles/theme/index.css +10 -0
  77. package/src/styles/theme/tokens.css +125 -0
  78. package/src/styles/theme/utilities.css +172 -0
  79. package/src/utils/debugInjector.js +261 -0
  80. package/src/utils/formatters.js +165 -0
  81. package/src/utils/index.js +35 -0
  82. package/src/utils/transformers.js +105 -0
@@ -0,0 +1,540 @@
1
+ /**
2
+ * EntityManager - Base class for entity CRUD operations
3
+ *
4
+ * An EntityManager can either:
5
+ * 1. BE its own storage (implement methods directly)
6
+ * 2. DELEGATE to a storage adapter
7
+ *
8
+ * This class provides the interface and optional delegation pattern.
9
+ *
10
+ * Usage (with delegation):
11
+ * ```js
12
+ * const users = new EntityManager({
13
+ * name: 'users',
14
+ * storage: new ApiStorage({ endpoint: '/users', client: axios }),
15
+ * // Metadata for UI
16
+ * label: 'User',
17
+ * labelPlural: 'Users',
18
+ * routePrefix: 'user',
19
+ * // Fields schema
20
+ * fields: {
21
+ * username: { type: 'text', label: 'Username', required: true },
22
+ * email: { type: 'email', label: 'Email', required: true },
23
+ * role: { type: 'select', label: 'Role', options: [...] }
24
+ * },
25
+ * children: {
26
+ * roles: { entity: 'roles' },
27
+ * sessions: { entity: 'sessions', endpoint: ':id/sessions' }
28
+ * }
29
+ * })
30
+ * ```
31
+ *
32
+ * Usage (direct implementation):
33
+ * ```js
34
+ * class UsersManager extends EntityManager {
35
+ * async list(params) { ... }
36
+ * async get(id) { ... }
37
+ * // etc.
38
+ * }
39
+ * ```
40
+ */
41
+ export class EntityManager {
42
+ constructor(options = {}) {
43
+ const {
44
+ name,
45
+ storage = null,
46
+ idField = 'id',
47
+ // Metadata for UI
48
+ label = null, // Singular label: 'User'
49
+ labelPlural = null, // Plural label: 'Users'
50
+ routePrefix = null, // Route prefix: 'user'
51
+ labelField = 'name', // Field to use as display label (breadcrumb, delete confirm, etc.)
52
+ // Fields schema
53
+ fields = {}, // { fieldName: { type, label, required, default, options, ... } }
54
+ // List behavior
55
+ localFilterThreshold = null, // Items threshold to switch to local filtering (null = use default)
56
+ // Relations
57
+ children = {}, // { roles: { entity: 'roles', endpoint?: ':id/roles' } }
58
+ parent = null, // { entity: 'users', foreignKey: 'user_id' }
59
+ relations = {} // { groups: { entity: 'groups', through: 'user_groups' } }
60
+ } = options
61
+
62
+ this.name = name
63
+ this.storage = storage
64
+ this.idField = idField
65
+ this._labelField = labelField // Can be string or function
66
+
67
+ // Metadata (with smart defaults)
68
+ this._label = label
69
+ this._labelPlural = labelPlural
70
+ this._routePrefix = routePrefix
71
+ this._fields = fields
72
+
73
+ // List behavior
74
+ this.localFilterThreshold = localFilterThreshold
75
+
76
+ // Relations
77
+ this._children = children
78
+ this._parent = parent
79
+ this._relations = relations
80
+ this._orchestrator = null // Set when registered
81
+ }
82
+
83
+ // ============ METADATA ACCESSORS ============
84
+
85
+ /**
86
+ * Get entity label (singular)
87
+ * Default: capitalize name (e.g., 'users' → 'User')
88
+ */
89
+ get label() {
90
+ if (this._label) return this._label
91
+ if (!this.name) return 'Item'
92
+ // users → User, book → Book
93
+ const singular = this.name.endsWith('s') ? this.name.slice(0, -1) : this.name
94
+ return singular.charAt(0).toUpperCase() + singular.slice(1)
95
+ }
96
+
97
+ /**
98
+ * Get entity label (plural)
99
+ * Default: capitalize name or add 's' (e.g., 'user' → 'Users')
100
+ */
101
+ get labelPlural() {
102
+ if (this._labelPlural) return this._labelPlural
103
+ if (!this.name) return 'Items'
104
+ // users → Users, book → Books
105
+ const plural = this.name.endsWith('s') ? this.name : this.name + 's'
106
+ return plural.charAt(0).toUpperCase() + plural.slice(1)
107
+ }
108
+
109
+ /**
110
+ * Get route prefix for this entity
111
+ * Default: singular form of name (e.g., 'books' → 'book')
112
+ */
113
+ get routePrefix() {
114
+ if (this._routePrefix) return this._routePrefix
115
+ if (!this.name) return 'item'
116
+ return this.name.endsWith('s') ? this.name.slice(0, -1) : this.name
117
+ }
118
+
119
+ /**
120
+ * Get labelField config (string or function)
121
+ * Used by components to determine how to display entity labels
122
+ */
123
+ get labelField() {
124
+ return this._labelField
125
+ }
126
+
127
+ /**
128
+ * Get display label for an entity
129
+ * Handles both string field name and callback function
130
+ * @param {object} entity - The entity object
131
+ * @returns {string|null} - The display label
132
+ */
133
+ getEntityLabel(entity) {
134
+ if (!entity) return null
135
+ if (typeof this._labelField === 'function') {
136
+ return this._labelField(entity)
137
+ }
138
+ return entity[this._labelField] || null
139
+ }
140
+
141
+ // ============ PERMISSIONS ============
142
+
143
+ /**
144
+ * Check if user can read entities
145
+ * Override in subclass or provide via options to implement custom logic
146
+ *
147
+ * @param {object} [entity] - Optional: specific entity to check
148
+ * @returns {boolean} - true if user can read
149
+ *
150
+ * Without entity: general read permission (e.g., can see the list)
151
+ * With entity: specific read permission (e.g., can see this item)
152
+ */
153
+ canRead(entity = null) {
154
+ // Default: allow all reads
155
+ return true
156
+ }
157
+
158
+ /**
159
+ * Check if user can create new entities
160
+ * Override in subclass to implement custom logic
161
+ *
162
+ * @returns {boolean} - true if user can create
163
+ */
164
+ canCreate() {
165
+ return true
166
+ }
167
+
168
+ /**
169
+ * Check if user can update entities
170
+ * Override in subclass to implement custom logic
171
+ *
172
+ * @param {object} [entity] - Optional: specific entity to check
173
+ * @returns {boolean} - true if user can update
174
+ *
175
+ * Without entity: general update permission
176
+ * With entity: specific update permission (e.g., can edit this item)
177
+ */
178
+ canUpdate(entity = null) {
179
+ return true
180
+ }
181
+
182
+ /**
183
+ * Check if user can delete entities
184
+ * Override in subclass or provide via options to implement custom logic
185
+ *
186
+ * @param {object} [entity] - Optional: specific entity to check
187
+ * @returns {boolean} - true if user can delete
188
+ */
189
+ canDelete(entity = null) {
190
+ return true
191
+ }
192
+
193
+ /**
194
+ * Get fields schema
195
+ */
196
+ get fields() {
197
+ return this._fields
198
+ }
199
+
200
+ /**
201
+ * Get a specific field config
202
+ * @param {string} fieldName
203
+ * @returns {object|undefined}
204
+ */
205
+ getFieldConfig(fieldName) {
206
+ return this._fields[fieldName]
207
+ }
208
+
209
+ /**
210
+ * Get initial data for a new entity based on field defaults
211
+ * @returns {object}
212
+ */
213
+ getInitialData() {
214
+ const data = {}
215
+ for (const [fieldName, fieldConfig] of Object.entries(this._fields)) {
216
+ if (fieldConfig.default !== undefined) {
217
+ data[fieldName] = typeof fieldConfig.default === 'function'
218
+ ? fieldConfig.default()
219
+ : fieldConfig.default
220
+ } else {
221
+ // Type-based defaults
222
+ switch (fieldConfig.type) {
223
+ case 'boolean':
224
+ data[fieldName] = false
225
+ break
226
+ case 'number':
227
+ data[fieldName] = null
228
+ break
229
+ case 'array':
230
+ data[fieldName] = []
231
+ break
232
+ case 'object':
233
+ data[fieldName] = {}
234
+ break
235
+ default:
236
+ data[fieldName] = ''
237
+ }
238
+ }
239
+ }
240
+ return data
241
+ }
242
+
243
+ /**
244
+ * Get field names that are required
245
+ * @returns {string[]}
246
+ */
247
+ getRequiredFields() {
248
+ return Object.entries(this._fields)
249
+ .filter(([, config]) => config.required)
250
+ .map(([name]) => name)
251
+ }
252
+
253
+ /**
254
+ * Get fields that should appear in list view
255
+ * @returns {Array<{name: string, ...config}>}
256
+ */
257
+ getListFields() {
258
+ return Object.entries(this._fields)
259
+ .filter(([, config]) => config.listable !== false)
260
+ .map(([name, config]) => ({ name, ...config }))
261
+ }
262
+
263
+ /**
264
+ * Get fields that should appear in form view
265
+ * @returns {Array<{name: string, ...config}>}
266
+ */
267
+ getFormFields() {
268
+ return Object.entries(this._fields)
269
+ .filter(([, config]) => config.editable !== false)
270
+ .map(([name, config]) => ({ name, ...config }))
271
+ }
272
+
273
+ /**
274
+ * List entities with pagination/filtering
275
+ * @param {object} params - { page, page_size, filters, sort_by, sort_order }
276
+ * @returns {Promise<{ items: Array, total: number }>}
277
+ */
278
+ async list(params = {}) {
279
+ if (this.storage) {
280
+ return this.storage.list(params)
281
+ }
282
+ throw new Error(`[EntityManager:${this.name}] list() not implemented`)
283
+ }
284
+
285
+ /**
286
+ * Get a single entity by ID
287
+ * @param {string|number} id
288
+ * @returns {Promise<object>}
289
+ */
290
+ async get(id) {
291
+ if (this.storage) {
292
+ return this.storage.get(id)
293
+ }
294
+ throw new Error(`[EntityManager:${this.name}] get() not implemented`)
295
+ }
296
+
297
+ /**
298
+ * Get multiple entities by IDs (batch fetch)
299
+ * @param {Array<string|number>} ids
300
+ * @returns {Promise<Array<object>>}
301
+ */
302
+ async getMany(ids) {
303
+ if (!ids || ids.length === 0) return []
304
+ if (this.storage?.getMany) {
305
+ return this.storage.getMany(ids)
306
+ }
307
+ // Fallback: parallel get calls
308
+ return Promise.all(ids.map(id => this.get(id).catch(() => null)))
309
+ .then(results => results.filter(Boolean))
310
+ }
311
+
312
+ /**
313
+ * Create a new entity
314
+ * @param {object} data
315
+ * @returns {Promise<object>} - The created entity
316
+ */
317
+ async create(data) {
318
+ if (this.storage) {
319
+ return this.storage.create(data)
320
+ }
321
+ throw new Error(`[EntityManager:${this.name}] create() not implemented`)
322
+ }
323
+
324
+ /**
325
+ * Update an entity (PUT - full replacement)
326
+ * @param {string|number} id
327
+ * @param {object} data
328
+ * @returns {Promise<object>}
329
+ */
330
+ async update(id, data) {
331
+ if (this.storage) {
332
+ return this.storage.update(id, data)
333
+ }
334
+ throw new Error(`[EntityManager:${this.name}] update() not implemented`)
335
+ }
336
+
337
+ /**
338
+ * Partially update an entity (PATCH)
339
+ * @param {string|number} id
340
+ * @param {object} data
341
+ * @returns {Promise<object>}
342
+ */
343
+ async patch(id, data) {
344
+ if (this.storage) {
345
+ return this.storage.patch(id, data)
346
+ }
347
+ throw new Error(`[EntityManager:${this.name}] patch() not implemented`)
348
+ }
349
+
350
+ /**
351
+ * Delete an entity
352
+ * @param {string|number} id
353
+ * @returns {Promise<void>}
354
+ */
355
+ async delete(id) {
356
+ if (this.storage) {
357
+ return this.storage.delete(id)
358
+ }
359
+ throw new Error(`[EntityManager:${this.name}] delete() not implemented`)
360
+ }
361
+
362
+ /**
363
+ * Generic request for special operations
364
+ * @param {string} method - 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'
365
+ * @param {string} path - Relative path
366
+ * @param {object} options - { data, params, headers }
367
+ * @returns {Promise<any>}
368
+ */
369
+ async request(method, path, options = {}) {
370
+ if (this.storage?.request) {
371
+ return this.storage.request(method, path, options)
372
+ }
373
+ throw new Error(`[EntityManager:${this.name}] request() not implemented`)
374
+ }
375
+
376
+ /**
377
+ * Hook: called when manager is registered with orchestrator
378
+ * Override to perform initialization
379
+ */
380
+ onRegister(orchestrator) {
381
+ this._orchestrator = orchestrator
382
+ }
383
+
384
+ // ============ RELATIONS ============
385
+
386
+ /**
387
+ * Get child relation config
388
+ * @param {string} childName - Child relation name
389
+ * @returns {object|undefined}
390
+ */
391
+ getChildConfig(childName) {
392
+ return this._children[childName]
393
+ }
394
+
395
+ /**
396
+ * Get all child relation names
397
+ * @returns {string[]}
398
+ */
399
+ getChildNames() {
400
+ return Object.keys(this._children)
401
+ }
402
+
403
+ /**
404
+ * Get parent relation config
405
+ * @returns {object|null}
406
+ */
407
+ getParentConfig() {
408
+ return this._parent
409
+ }
410
+
411
+ /**
412
+ * List children of a parent entity
413
+ * @param {string|number} parentId - Parent entity ID
414
+ * @param {string} childName - Child relation name
415
+ * @param {object} params - List params
416
+ * @returns {Promise<{ items: Array, total: number }>}
417
+ */
418
+ async listChildren(parentId, childName, params = {}) {
419
+ const childConfig = this._children[childName]
420
+ if (!childConfig) {
421
+ throw new Error(`[EntityManager:${this.name}] Unknown child relation "${childName}"`)
422
+ }
423
+
424
+ // Build endpoint path
425
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
426
+
427
+ if (this.storage?.request) {
428
+ return this.storage.request('GET', childEndpoint, { params })
429
+ }
430
+
431
+ throw new Error(`[EntityManager:${this.name}] listChildren() requires storage with request()`)
432
+ }
433
+
434
+ /**
435
+ * Get a specific child entity
436
+ * @param {string|number} parentId - Parent entity ID
437
+ * @param {string} childName - Child relation name
438
+ * @param {string|number} childId - Child entity ID
439
+ * @returns {Promise<object>}
440
+ */
441
+ async getChild(parentId, childName, childId) {
442
+ const childConfig = this._children[childName]
443
+ if (!childConfig) {
444
+ throw new Error(`[EntityManager:${this.name}] Unknown child relation "${childName}"`)
445
+ }
446
+
447
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
448
+
449
+ if (this.storage?.request) {
450
+ return this.storage.request('GET', `${childEndpoint}/${childId}`)
451
+ }
452
+
453
+ throw new Error(`[EntityManager:${this.name}] getChild() requires storage with request()`)
454
+ }
455
+
456
+ /**
457
+ * Create a child entity
458
+ * @param {string|number} parentId - Parent entity ID
459
+ * @param {string} childName - Child relation name
460
+ * @param {object} data - Child entity data
461
+ * @returns {Promise<object>}
462
+ */
463
+ async createChild(parentId, childName, data) {
464
+ const childConfig = this._children[childName]
465
+ if (!childConfig) {
466
+ throw new Error(`[EntityManager:${this.name}] Unknown child relation "${childName}"`)
467
+ }
468
+
469
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
470
+
471
+ if (this.storage?.request) {
472
+ return this.storage.request('POST', childEndpoint, { data })
473
+ }
474
+
475
+ throw new Error(`[EntityManager:${this.name}] createChild() requires storage with request()`)
476
+ }
477
+
478
+ /**
479
+ * Delete a child entity
480
+ * @param {string|number} parentId - Parent entity ID
481
+ * @param {string} childName - Child relation name
482
+ * @param {string|number} childId - Child entity ID
483
+ * @returns {Promise<void>}
484
+ */
485
+ async deleteChild(parentId, childName, childId) {
486
+ const childConfig = this._children[childName]
487
+ if (!childConfig) {
488
+ throw new Error(`[EntityManager:${this.name}] Unknown child relation "${childName}"`)
489
+ }
490
+
491
+ const childEndpoint = childConfig.endpoint || `${parentId}/${childName}`
492
+
493
+ if (this.storage?.request) {
494
+ return this.storage.request('DELETE', `${childEndpoint}/${childId}`)
495
+ }
496
+
497
+ throw new Error(`[EntityManager:${this.name}] deleteChild() requires storage with request()`)
498
+ }
499
+
500
+ /**
501
+ * Get the parent entity's manager
502
+ * @returns {EntityManager|null}
503
+ */
504
+ getParentManager() {
505
+ if (!this._parent || !this._orchestrator) return null
506
+ return this._orchestrator.get(this._parent.entity)
507
+ }
508
+
509
+ /**
510
+ * Get a child entity's manager (from orchestrator)
511
+ * @param {string} childName - Child relation name
512
+ * @returns {EntityManager|null}
513
+ */
514
+ getChildManager(childName) {
515
+ const childConfig = this._children[childName]
516
+ if (!childConfig || !this._orchestrator) return null
517
+ return this._orchestrator.get(childConfig.entity)
518
+ }
519
+
520
+ /**
521
+ * Hook: called when orchestrator is disposed
522
+ * Override to perform cleanup
523
+ */
524
+ onDispose() {
525
+ // Override in subclass if needed
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Factory function to create an EntityManager
531
+ *
532
+ * @param {object} options
533
+ * @param {string} options.name - Entity name
534
+ * @param {object} [options.storage] - Storage adapter instance
535
+ * @param {string} [options.idField='id'] - Field name for entity ID
536
+ * @returns {EntityManager}
537
+ */
538
+ export function createEntityManager(options) {
539
+ return new EntityManager(options)
540
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Entity Module
3
+ *
4
+ * EntityManager and storage adapters for data abstraction.
5
+ */
6
+
7
+ // EntityManager
8
+ export { EntityManager, createEntityManager } from './EntityManager.js'
9
+
10
+ // Storage adapters
11
+ export * from './storage/index.js'
@@ -0,0 +1,146 @@
1
+ /**
2
+ * ApiStorage - REST API storage adapter
3
+ *
4
+ * Implements the storage interface for REST APIs.
5
+ * Expects standard response format: { items: [], total: number, page: number }
6
+ *
7
+ * Usage:
8
+ * ```js
9
+ * import axios from 'axios'
10
+ *
11
+ * const storage = new ApiStorage({
12
+ * endpoint: '/users',
13
+ * client: axios.create({ baseURL: '/api' })
14
+ * })
15
+ *
16
+ * // Or with callback for dynamic client
17
+ * const storage = new ApiStorage({
18
+ * endpoint: '/users',
19
+ * getClient: () => inject('apiClient')
20
+ * })
21
+ * ```
22
+ */
23
+ export class ApiStorage {
24
+ constructor(options = {}) {
25
+ const {
26
+ endpoint,
27
+ client = null,
28
+ getClient = null,
29
+ // Response format configuration
30
+ responseItemsKey = 'items',
31
+ responseTotalKey = 'total'
32
+ } = options
33
+
34
+ this.endpoint = endpoint
35
+ this._client = client
36
+ this._getClient = getClient
37
+ this.responseItemsKey = responseItemsKey
38
+ this.responseTotalKey = responseTotalKey
39
+ }
40
+
41
+ get client() {
42
+ if (this._getClient) {
43
+ return this._getClient()
44
+ }
45
+ return this._client
46
+ }
47
+
48
+ set client(value) {
49
+ this._client = value
50
+ }
51
+
52
+ /**
53
+ * List entities with pagination/filtering
54
+ * @param {object} params - { page, page_size, filters, sort_by, sort_order }
55
+ * @returns {Promise<{ items: Array, total: number }>}
56
+ */
57
+ async list(params = {}) {
58
+ const { page = 1, page_size = 20, ...filters } = params
59
+ const response = await this.client.get(this.endpoint, {
60
+ params: { page, page_size, ...filters }
61
+ })
62
+
63
+ const data = response.data
64
+ return {
65
+ items: data[this.responseItemsKey] || data.items || data,
66
+ total: data[this.responseTotalKey] || data.total || (Array.isArray(data) ? data.length : 0)
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Get a single entity by ID
72
+ * @param {string|number} id
73
+ * @returns {Promise<object>}
74
+ */
75
+ async get(id) {
76
+ const response = await this.client.get(`${this.endpoint}/${id}`)
77
+ return response.data
78
+ }
79
+
80
+ /**
81
+ * Create a new entity
82
+ * @param {object} data
83
+ * @returns {Promise<object>}
84
+ */
85
+ async create(data) {
86
+ const response = await this.client.post(this.endpoint, data)
87
+ return response.data
88
+ }
89
+
90
+ /**
91
+ * Update an entity (PUT - full replacement)
92
+ * @param {string|number} id
93
+ * @param {object} data
94
+ * @returns {Promise<object>}
95
+ */
96
+ async update(id, data) {
97
+ const response = await this.client.put(`${this.endpoint}/${id}`, data)
98
+ return response.data
99
+ }
100
+
101
+ /**
102
+ * Partially update an entity (PATCH)
103
+ * @param {string|number} id
104
+ * @param {object} data
105
+ * @returns {Promise<object>}
106
+ */
107
+ async patch(id, data) {
108
+ const response = await this.client.patch(`${this.endpoint}/${id}`, data)
109
+ return response.data
110
+ }
111
+
112
+ /**
113
+ * Delete an entity
114
+ * @param {string|number} id
115
+ * @returns {Promise<void>}
116
+ */
117
+ async delete(id) {
118
+ await this.client.delete(`${this.endpoint}/${id}`)
119
+ }
120
+
121
+ /**
122
+ * Generic request for special operations
123
+ * @param {string} method - 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'
124
+ * @param {string} path - Relative path (appended to endpoint)
125
+ * @param {object} options - { data, params, headers }
126
+ * @returns {Promise<any>}
127
+ */
128
+ async request(method, path, options = {}) {
129
+ const url = path.startsWith('/') ? path : `${this.endpoint}/${path}`
130
+ const response = await this.client.request({
131
+ method,
132
+ url,
133
+ data: options.data,
134
+ params: options.params,
135
+ headers: options.headers
136
+ })
137
+ return response.data
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Factory function to create an ApiStorage
143
+ */
144
+ export function createApiStorage(options) {
145
+ return new ApiStorage(options)
146
+ }