@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 +1 -1
- package/src/entity/storage/LocalStorage.ts +10 -41
- package/src/entity/storage/MemoryStorage.ts +10 -39
- package/src/entity/storage/MockApiStorage.ts +8 -30
- package/src/entity/storage/SdkStorage.ts +6 -26
- package/src/entity/storage/errors.ts +15 -0
- package/src/query/clientFilter.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IStorage } from './IStorage'
|
|
2
2
|
import type { EntityRecord, ListParams, ListResult, StorageCapabilities } from '../../types'
|
|
3
|
-
import { StorageError } from './
|
|
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 =
|
|
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 (
|
|
66
|
-
|
|
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 (
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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 './
|
|
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 =
|
|
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 (
|
|
148
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|