@quazardous/qdadm 2.3.0 → 2.3.1

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": "@quazardous/qdadm",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -1,6 +1,7 @@
1
1
  import { IStorage } from './IStorage'
2
2
  import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
3
- import { StorageError } from './MemoryStorage'
3
+ import { StorageError } from './errors'
4
+ import { sortItems, filterItems, searchItems, paginate, defaultGenerateId } from '../../query/clientFilter'
4
5
 
5
6
  /**
6
7
  * LocalStorage options
@@ -36,7 +37,7 @@ export class LocalStorage<T extends EntityRecord = EntityRecord> extends IStorag
36
37
  const {
37
38
  key,
38
39
  idField = 'id',
39
- generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
40
+ generateId = defaultGenerateId,
40
41
  } = options
41
42
 
42
43
  this.key = key
@@ -62,49 +63,17 @@ export class LocalStorage<T extends EntityRecord = EntityRecord> extends IStorag
62
63
 
63
64
  let items = this._getAll()
64
65
 
65
- // Apply filters (exact match for dropdown filters)
66
- for (const [key, value] of Object.entries(filters)) {
67
- if (value === null || value === undefined || value === '') continue
68
- items = items.filter((item) => {
69
- const itemValue = item[key as keyof T]
70
- if (typeof value === 'string' && typeof itemValue === 'string') {
71
- return itemValue.toLowerCase() === value.toLowerCase()
72
- }
73
- return itemValue === value
74
- })
75
- }
66
+ // Apply filters (shared pipeline, #1192 — legacy exact-match semantics)
67
+ items = filterItems(items, filters, { stringMatch: 'exact' })
76
68
 
77
- // Apply search (substring match on all string fields)
78
- if (search && typeof search === 'string' && search.trim()) {
79
- const query = search.toLowerCase().trim()
80
- items = items.filter((item) => {
81
- for (const value of Object.values(item)) {
82
- if (typeof value === 'string' && value.toLowerCase().includes(query)) {
83
- return true
84
- }
85
- }
86
- return false
87
- })
88
- }
69
+ // Apply search (shared pipeline, #1192)
70
+ items = searchItems(items, search as string | undefined)
89
71
 
90
72
  const total = items.length
91
73
 
92
- // Apply sorting
93
- if (sort_by) {
94
- items.sort((a, b) => {
95
- const aVal = a[sort_by as keyof T]
96
- const bVal = b[sort_by as keyof T]
97
- if (aVal === undefined || aVal === null) return 1
98
- if (bVal === undefined || bVal === null) return -1
99
- if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
100
- if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
101
- return 0
102
- })
103
- }
104
-
105
- // Apply pagination
106
- const start = (page - 1) * page_size
107
- items = items.slice(start, start + page_size)
74
+ // Apply sorting + pagination (shared pipeline, #1192)
75
+ sortItems(items, sort_by, sort_order)
76
+ items = paginate(items, page, page_size)
108
77
 
109
78
  return { items, total }
110
79
  }
@@ -1,5 +1,7 @@
1
1
  import { IStorage } from './IStorage'
2
2
  import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
3
+ import { StorageError } from './errors'
4
+ import { sortItems, filterItems, paginate, defaultGenerateId } from '../../query/clientFilter'
3
5
 
4
6
  /**
5
7
  * MemoryStorage options
@@ -10,17 +12,8 @@ export interface MemoryStorageOptions<T extends EntityRecord = EntityRecord> {
10
12
  initialData?: T[]
11
13
  }
12
14
 
13
- /**
14
- * Storage error with HTTP-like status
15
- */
16
- export class StorageError extends Error {
17
- status: number
18
- constructor(message: string, status: number) {
19
- super(message)
20
- this.status = status
21
- this.name = 'StorageError'
22
- }
23
- }
15
+ // StorageError moved to ./errors (#1192); re-exported for back-compat.
16
+ export { StorageError } from './errors'
24
17
 
25
18
  /**
26
19
  * MemoryStorage - In-memory storage adapter
@@ -46,7 +39,7 @@ export class MemoryStorage<T extends EntityRecord = EntityRecord> extends IStora
46
39
  super()
47
40
  const {
48
41
  idField = 'id',
49
- generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
42
+ generateId = defaultGenerateId,
50
43
  initialData = [],
51
44
  } = options
52
45
 
@@ -72,36 +65,14 @@ export class MemoryStorage<T extends EntityRecord = EntityRecord> extends IStora
72
65
 
73
66
  let items = this._getAll()
74
67
 
75
- // Apply filters
76
- for (const [key, value] of Object.entries(filters)) {
77
- if (value === null || value === undefined || value === '') continue
78
- items = items.filter((item) => {
79
- const itemValue = item[key as keyof T]
80
- if (typeof value === 'string' && typeof itemValue === 'string') {
81
- return itemValue.toLowerCase().includes(value.toLowerCase())
82
- }
83
- return itemValue === value
84
- })
85
- }
68
+ // Apply filters (shared pipeline, #1192 — legacy substring semantics)
69
+ items = filterItems(items, filters, { stringMatch: 'includes' })
86
70
 
87
71
  const total = items.length
88
72
 
89
- // Apply sorting
90
- if (sort_by) {
91
- items.sort((a, b) => {
92
- const aVal = a[sort_by as keyof T]
93
- const bVal = b[sort_by as keyof T]
94
- if (aVal === undefined || aVal === null) return 1
95
- if (bVal === undefined || bVal === null) return -1
96
- if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
97
- if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
98
- return 0
99
- })
100
- }
101
-
102
- // Apply pagination
103
- const start = (page - 1) * page_size
104
- items = items.slice(start, start + page_size)
73
+ // Apply sorting + pagination (shared pipeline, #1192)
74
+ sortItems(items, sort_by, sort_order)
75
+ items = paginate(items, page, page_size)
105
76
 
106
77
  return { items, total }
107
78
  }
@@ -1,7 +1,8 @@
1
1
  import { IStorage } from './IStorage'
2
- import { StorageError } from './MemoryStorage'
2
+ import { StorageError } from './errors'
3
3
  import { QueryExecutor } from '../../query'
4
4
  import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
5
+ import { sortItems, searchItems, paginate, defaultGenerateId } from '../../query/clientFilter'
5
6
 
6
7
  /**
7
8
  * MockApiStorage options
@@ -59,7 +60,7 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
59
60
  const {
60
61
  entityName,
61
62
  idField = 'id',
62
- generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
63
+ generateId = defaultGenerateId,
63
64
  initialData = null,
64
65
  authCheck = null,
65
66
  } = options
@@ -144,37 +145,14 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
144
145
  .items as T[]
145
146
  }
146
147
 
147
- // Apply search (substring match on all string fields)
148
- if (search && typeof search === 'string' && search.trim()) {
149
- const query = search.toLowerCase().trim()
150
- items = items.filter((item) => {
151
- for (const value of Object.values(item)) {
152
- if (typeof value === 'string' && value.toLowerCase().includes(query)) {
153
- return true
154
- }
155
- }
156
- return false
157
- })
158
- }
148
+ // Apply search (shared pipeline, #1192)
149
+ items = searchItems(items, search as string | undefined)
159
150
 
160
151
  const total = items.length
161
152
 
162
- // Apply sorting
163
- if (sort_by) {
164
- items.sort((a, b) => {
165
- const aVal = a[sort_by as keyof T]
166
- const bVal = b[sort_by as keyof T]
167
- if (aVal === undefined || aVal === null) return 1
168
- if (bVal === undefined || bVal === null) return -1
169
- if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
170
- if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
171
- return 0
172
- })
173
- }
174
-
175
- // Apply pagination
176
- const start = (page - 1) * page_size
177
- items = items.slice(start, start + page_size)
153
+ // Apply sorting + pagination (shared pipeline, #1192)
154
+ sortItems(items, sort_by, sort_order)
155
+ items = paginate(items, page, page_size)
178
156
 
179
157
  return { items, total }
180
158
  }
@@ -1,5 +1,6 @@
1
1
  import { IStorage } from './IStorage'
2
2
  import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
3
+ import { sortItems, filterItems, paginate } from '../../query/clientFilter'
3
4
 
4
5
  /**
5
6
  * SDK method result with data property
@@ -289,33 +290,12 @@ export class SdkStorage<T extends EntityRecord = EntityRecord> extends IStorage<
289
290
  if (this.clientSidePagination && items.length > 0) {
290
291
  total = items.length
291
292
 
292
- if (sort_by) {
293
- items = [...items].sort((a, b) => {
294
- const aVal = a[sort_by as keyof T]
295
- const bVal = b[sort_by as keyof T]
296
- if (aVal === undefined || aVal === null) return 1
297
- if (bVal === undefined || bVal === null) return -1
298
- if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
299
- if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
300
- return 0
301
- })
302
- }
303
-
304
- for (const [key, value] of Object.entries(filters)) {
305
- if (value === null || value === undefined || value === '') continue
306
- items = items.filter((item) => {
307
- const itemValue = item[key as keyof T]
308
- if (typeof value === 'string' && typeof itemValue === 'string') {
309
- return itemValue.toLowerCase().includes(value.toLowerCase())
310
- }
311
- return itemValue === value
312
- })
313
- }
314
-
293
+ // Shared pipeline (#1192) — original order preserved: sort, then
294
+ // filter (substring semantics), then paginate.
295
+ items = sortItems([...items], sort_by, sort_order)
296
+ items = filterItems(items, filters, { stringMatch: 'includes' })
315
297
  total = items.length
316
-
317
- const start = (page - 1) * page_size
318
- items = items.slice(start, start + page_size)
298
+ items = paginate(items, page, page_size)
319
299
  }
320
300
 
321
301
  return { items, total }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Storage error types (#1192) — moved out of MemoryStorage so sibling
3
+ * adapters no longer cross-import from one another.
4
+ */
5
+
6
+ /** Error with an HTTP-like status code, thrown by storage adapters. */
7
+ export class StorageError extends Error {
8
+ status: number
9
+
10
+ constructor(message: string, status = 500) {
11
+ super(message)
12
+ this.name = 'StorageError'
13
+ this.status = status
14
+ }
15
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared client-side list pipeline (#1192, KPI-5)
3
+ *
4
+ * The sort / search / paginate blocks below were byte-identical across
5
+ * MemoryStorage, LocalStorage, MockApiStorage, SdkStorage and
6
+ * EntityManager.query — one implementation now serves them all.
7
+ *
8
+ * Filtering intentionally takes a `stringMatch` mode because the adapters
9
+ * historically diverged (Memory/Sdk: substring `includes`, Local: exact
10
+ * case-insensitive) and this refactor is behavior-preserving. MockApiStorage
11
+ * keeps delegating operator filters to QueryExecutor; converging every
12
+ * adapter onto QueryExecutor would change plain-string semantics and is
13
+ * out of scope here.
14
+ */
15
+
16
+ export type SortOrder = 'asc' | 'desc'
17
+
18
+ /** Sort by a field, nulls/undefined last — the canonical adapter comparator. */
19
+ export function sortItems<T>(items: T[], sortBy?: string | null, sortOrder: SortOrder = 'asc'): T[] {
20
+ if (!sortBy) return items
21
+ return items.sort((a, b) => {
22
+ const aVal = a[sortBy as keyof T]
23
+ const bVal = b[sortBy as keyof T]
24
+ if (aVal === undefined || aVal === null) return 1
25
+ if (bVal === undefined || bVal === null) return -1
26
+ if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1
27
+ if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1
28
+ return 0
29
+ })
30
+ }
31
+
32
+ export interface FilterItemsOptions {
33
+ /**
34
+ * How string filter values match string item values:
35
+ * - 'includes': case-insensitive substring (MemoryStorage/SdkStorage legacy)
36
+ * - 'exact': case-insensitive equality (LocalStorage legacy)
37
+ */
38
+ stringMatch?: 'includes' | 'exact'
39
+ }
40
+
41
+ /** Apply simple field filters; empty/null/undefined filter values are skipped. */
42
+ export function filterItems<T>(
43
+ items: T[],
44
+ filters: Record<string, unknown> = {},
45
+ options: FilterItemsOptions = {}
46
+ ): T[] {
47
+ const { stringMatch = 'includes' } = options
48
+ let result = items
49
+ for (const [key, value] of Object.entries(filters)) {
50
+ if (value === null || value === undefined || value === '') continue
51
+ result = result.filter((item) => {
52
+ const itemValue = item[key as keyof T]
53
+ if (typeof value === 'string' && typeof itemValue === 'string') {
54
+ return stringMatch === 'exact'
55
+ ? itemValue.toLowerCase() === value.toLowerCase()
56
+ : itemValue.toLowerCase().includes(value.toLowerCase())
57
+ }
58
+ return itemValue === value
59
+ })
60
+ }
61
+ return result
62
+ }
63
+
64
+ /** Substring match on every string field (the adapters' full-text search). */
65
+ export function searchItems<T>(items: T[], search?: string | null): T[] {
66
+ if (!search || typeof search !== 'string' || !search.trim()) return items
67
+ const query = search.toLowerCase().trim()
68
+ return items.filter((item) => {
69
+ for (const value of Object.values(item as Record<string, unknown>)) {
70
+ if (typeof value === 'string' && value.toLowerCase().includes(query)) {
71
+ return true
72
+ }
73
+ }
74
+ return false
75
+ })
76
+ }
77
+
78
+ /** Slice a page out of the item list. */
79
+ export function paginate<T>(items: T[], page = 1, pageSize = 20): T[] {
80
+ const start = (page - 1) * pageSize
81
+ return items.slice(start, start + pageSize)
82
+ }
83
+
84
+ /** Default id generator for in-memory adapters (was triplicated, with a deprecated substr). */
85
+ export function defaultGenerateId(): string {
86
+ return Date.now().toString(36) + Math.random().toString(36).slice(2)
87
+ }