qdadm 0.18.0 → 0.26.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 +1 -1
- package/src/composables/useListPageBuilder.js +301 -49
- package/src/entity/EntityManager.js +293 -24
- package/src/entity/storage/ApiStorage.js +19 -2
- package/src/entity/storage/LocalStorage.js +25 -2
- package/src/entity/storage/MemoryStorage.js +28 -0
- package/src/entity/storage/MockApiStorage.js +25 -2
- package/src/entity/storage/SdkStorage.js +17 -2
- package/src/entity/storage/index.js +105 -0
- package/src/index.js +3 -0
- package/src/query/FilterQuery.js +277 -0
- package/src/query/QueryExecutor.js +332 -0
- package/src/query/index.js +8 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PermissiveAuthAdapter } from './auth/PermissiveAdapter.js'
|
|
2
2
|
import { AuthActions } from './auth/AuthAdapter.js'
|
|
3
|
+
import { QueryExecutor } from '../query/QueryExecutor.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* EntityManager - Base class for entity CRUD operations
|
|
@@ -62,6 +63,7 @@ export class EntityManager {
|
|
|
62
63
|
// Relations
|
|
63
64
|
children = {}, // { roles: { entity: 'roles', endpoint?: ':id/roles' } }
|
|
64
65
|
parent = null, // { entity: 'users', foreignKey: 'user_id' }
|
|
66
|
+
parents = {}, // { book: { entity: 'books', foreignKey: 'book_id' } } - multi-parent support
|
|
65
67
|
relations = {}, // { groups: { entity: 'groups', through: 'user_groups' } }
|
|
66
68
|
// Auth adapter (for permission checks)
|
|
67
69
|
authAdapter = null // AuthAdapter instance or null (uses PermissiveAuthAdapter)
|
|
@@ -88,6 +90,7 @@ export class EntityManager {
|
|
|
88
90
|
// Relations
|
|
89
91
|
this._children = children
|
|
90
92
|
this._parent = parent
|
|
93
|
+
this._parents = parents
|
|
91
94
|
this._relations = relations
|
|
92
95
|
this._orchestrator = null // Set when registered
|
|
93
96
|
|
|
@@ -111,6 +114,9 @@ export class EntityManager {
|
|
|
111
114
|
|
|
112
115
|
// SignalBus reference for event emission (set by Orchestrator)
|
|
113
116
|
this._signals = null
|
|
117
|
+
|
|
118
|
+
// Cleanup function for signal listeners
|
|
119
|
+
this._signalCleanup = null
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
// ============ SIGNALS ============
|
|
@@ -121,6 +127,7 @@ export class EntityManager {
|
|
|
121
127
|
*/
|
|
122
128
|
setSignals(signals) {
|
|
123
129
|
this._signals = signals
|
|
130
|
+
this._setupCacheListeners()
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
/**
|
|
@@ -687,6 +694,7 @@ export class EntityManager {
|
|
|
687
694
|
|
|
688
695
|
// 1. Cache valid + cacheable → use cache with local filtering
|
|
689
696
|
if (this._cache.valid && canUseCache) {
|
|
697
|
+
console.log('[cache] Using local cache for entity:', this.name)
|
|
690
698
|
const filtered = this._filterLocally(this._cache.items, queryParams)
|
|
691
699
|
return { ...filtered, fromCache: true }
|
|
692
700
|
}
|
|
@@ -696,12 +704,17 @@ export class EntityManager {
|
|
|
696
704
|
const items = response.items || []
|
|
697
705
|
const total = response.total ?? items.length
|
|
698
706
|
|
|
699
|
-
// 3. Fill cache opportunistically if
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
707
|
+
// 3. Fill cache opportunistically if:
|
|
708
|
+
// - canUseCache (no filters or cacheSafe filters)
|
|
709
|
+
// - isCacheEnabled (threshold > 0 and storage supports total)
|
|
710
|
+
// - total <= threshold (all items fit in cache for complete local filtering)
|
|
711
|
+
if (canUseCache && this.isCacheEnabled && total <= this.effectiveThreshold) {
|
|
712
|
+
this._cache.items = items // Store all items (no slicing - total fits threshold)
|
|
713
|
+
this._cache.total = total
|
|
703
714
|
this._cache.valid = true
|
|
704
715
|
this._cache.loadedAt = Date.now()
|
|
716
|
+
// Resolve parent fields for search (book.title, user.username, etc.)
|
|
717
|
+
await this._resolveSearchFields(items)
|
|
705
718
|
}
|
|
706
719
|
|
|
707
720
|
return { items, total, fromCache: false }
|
|
@@ -896,16 +909,196 @@ export class EntityManager {
|
|
|
896
909
|
return this.localFilterThreshold ?? 100
|
|
897
910
|
}
|
|
898
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Check if storage adapter supports returning total count
|
|
914
|
+
*
|
|
915
|
+
* Auto-caching only makes sense when storage can report total items
|
|
916
|
+
* (to compare against threshold). Reads from storage.constructor.capabilities.supportsTotal.
|
|
917
|
+
*
|
|
918
|
+
* @returns {boolean} - true if storage supports total, false otherwise
|
|
919
|
+
*/
|
|
920
|
+
get storageSupportsTotal() {
|
|
921
|
+
const caps = this.storage?.constructor?.capabilities || {}
|
|
922
|
+
return caps.supportsTotal ?? false
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Get searchable fields declared by storage adapter
|
|
927
|
+
*
|
|
928
|
+
* Returns the list of fields that should be searched in _filterLocally().
|
|
929
|
+
* Supports both own fields ('title') and parent entity fields ('book.title').
|
|
930
|
+
* When undefined (not declared), all string fields are searched.
|
|
931
|
+
*
|
|
932
|
+
* @returns {string[]|undefined} - Array of field names or undefined
|
|
933
|
+
*/
|
|
934
|
+
get storageSearchFields() {
|
|
935
|
+
const caps = this.storage?.constructor?.capabilities || {}
|
|
936
|
+
return caps.searchFields
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Parse searchFields into own fields and parent fields
|
|
941
|
+
*
|
|
942
|
+
* Separates fields without dots (own fields like 'title') from fields with
|
|
943
|
+
* dots (parent fields like 'book.title'). Groups parent fields by parentKey.
|
|
944
|
+
*
|
|
945
|
+
* @returns {{ ownFields: string[], parentFields: Object<string, string[]> }}
|
|
946
|
+
*/
|
|
947
|
+
_parseSearchFields(overrideSearchFields = null) {
|
|
948
|
+
const searchFields = overrideSearchFields ?? this.storageSearchFields ?? []
|
|
949
|
+
const ownFields = []
|
|
950
|
+
const parentFields = {}
|
|
951
|
+
|
|
952
|
+
for (const field of searchFields) {
|
|
953
|
+
if (!field.includes('.')) {
|
|
954
|
+
ownFields.push(field)
|
|
955
|
+
continue
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Split on first dot only
|
|
959
|
+
const [parentKey, fieldName] = field.split('.', 2)
|
|
960
|
+
|
|
961
|
+
// Validate parentKey exists in parents config
|
|
962
|
+
if (!this._parents[parentKey]) {
|
|
963
|
+
console.warn(`[EntityManager:${this.name}] Unknown parent '${parentKey}' in searchFields '${field}', skipping`)
|
|
964
|
+
continue
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Group by parentKey
|
|
968
|
+
if (!parentFields[parentKey]) {
|
|
969
|
+
parentFields[parentKey] = []
|
|
970
|
+
}
|
|
971
|
+
parentFields[parentKey].push(fieldName)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return { ownFields, parentFields }
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Resolve parent entity fields for searchable caching
|
|
979
|
+
*
|
|
980
|
+
* Batch-fetches parent entities and caches their field values on each item
|
|
981
|
+
* in a non-enumerable `_search` property to avoid JSON serialization issues.
|
|
982
|
+
*
|
|
983
|
+
* @param {Array} items - Items to resolve parent fields for
|
|
984
|
+
* @returns {Promise<void>}
|
|
985
|
+
*/
|
|
986
|
+
async _resolveSearchFields(items) {
|
|
987
|
+
const { parentFields } = this._parseSearchFields()
|
|
988
|
+
|
|
989
|
+
// No parent fields to resolve
|
|
990
|
+
if (Object.keys(parentFields).length === 0) return
|
|
991
|
+
|
|
992
|
+
// Need orchestrator to access other managers
|
|
993
|
+
if (!this._orchestrator) {
|
|
994
|
+
console.warn(`[EntityManager:${this.name}] No orchestrator, cannot resolve parent fields`)
|
|
995
|
+
return
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Process each parent type
|
|
999
|
+
for (const [parentKey, fields] of Object.entries(parentFields)) {
|
|
1000
|
+
const config = this._parents[parentKey]
|
|
1001
|
+
|
|
1002
|
+
if (!config) {
|
|
1003
|
+
console.warn(`[EntityManager:${this.name}] Missing parent config for '${parentKey}'`)
|
|
1004
|
+
continue
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Extract unique parent IDs (deduplicated)
|
|
1008
|
+
const ids = [...new Set(items.map(i => i[config.foreignKey]).filter(Boolean))]
|
|
1009
|
+
if (ids.length === 0) continue
|
|
1010
|
+
|
|
1011
|
+
// Batch fetch parent entities
|
|
1012
|
+
const manager = this._orchestrator.get(config.entity)
|
|
1013
|
+
if (!manager) {
|
|
1014
|
+
console.warn(`[EntityManager:${this.name}] Manager not found for '${config.entity}'`)
|
|
1015
|
+
continue
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const parents = await manager.getMany(ids)
|
|
1019
|
+
const parentMap = new Map(parents.map(p => [p[manager.idField], p]))
|
|
1020
|
+
|
|
1021
|
+
// Cache resolved values in _search (non-enumerable)
|
|
1022
|
+
for (const item of items) {
|
|
1023
|
+
// Initialize _search as non-enumerable if needed
|
|
1024
|
+
if (!item._search) {
|
|
1025
|
+
Object.defineProperty(item, '_search', {
|
|
1026
|
+
value: {},
|
|
1027
|
+
enumerable: false,
|
|
1028
|
+
writable: true,
|
|
1029
|
+
configurable: true
|
|
1030
|
+
})
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const parent = parentMap.get(item[config.foreignKey])
|
|
1034
|
+
for (const field of fields) {
|
|
1035
|
+
item._search[`${parentKey}.${field}`] = parent?.[field] ?? ''
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Set up signal listeners for parent entity cache invalidation
|
|
1043
|
+
*
|
|
1044
|
+
* When a parent entity is modified, clears the _search cache on cached items
|
|
1045
|
+
* so that next list() will re-resolve with fresh parent data.
|
|
1046
|
+
*/
|
|
1047
|
+
_setupCacheListeners() {
|
|
1048
|
+
// Nothing to do if no parents or no signals
|
|
1049
|
+
if (!this._parents || Object.keys(this._parents).length === 0) return
|
|
1050
|
+
if (!this._signals) return
|
|
1051
|
+
|
|
1052
|
+
// Clean up existing listener if any
|
|
1053
|
+
if (this._signalCleanup) {
|
|
1054
|
+
this._signalCleanup()
|
|
1055
|
+
this._signalCleanup = null
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Get parent entity names
|
|
1059
|
+
const parentEntities = Object.values(this._parents).map(p => p.entity)
|
|
1060
|
+
|
|
1061
|
+
// Listen for parent cache invalidation
|
|
1062
|
+
this._signalCleanup = this._signals.on('cache:entity:invalidated', ({ entity }) => {
|
|
1063
|
+
if (parentEntities.includes(entity)) {
|
|
1064
|
+
this._clearSearchCache()
|
|
1065
|
+
}
|
|
1066
|
+
})
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Clear the _search cache from all cached items
|
|
1071
|
+
*
|
|
1072
|
+
* Called when a parent entity is invalidated. Forces refetch and
|
|
1073
|
+
* re-resolution of parent fields on next list() call.
|
|
1074
|
+
*/
|
|
1075
|
+
_clearSearchCache() {
|
|
1076
|
+
if (!this._cache.valid) return
|
|
1077
|
+
|
|
1078
|
+
for (const item of this._cache.items) {
|
|
1079
|
+
if (item._search) {
|
|
1080
|
+
item._search = {}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Invalidate cache so next list() refetches
|
|
1085
|
+
this.invalidateCache()
|
|
1086
|
+
}
|
|
1087
|
+
|
|
899
1088
|
/**
|
|
900
1089
|
* Check if caching is enabled for this entity
|
|
1090
|
+
*
|
|
901
1091
|
* Caching is disabled if:
|
|
902
1092
|
* - threshold is 0 (explicit disable)
|
|
903
1093
|
* - storage declares supportsCaching = false (e.g., LocalStorage)
|
|
1094
|
+
* - storage does not support total count (cannot determine if items fit threshold)
|
|
1095
|
+
*
|
|
904
1096
|
* @returns {boolean}
|
|
905
1097
|
*/
|
|
906
1098
|
get isCacheEnabled() {
|
|
907
1099
|
if (this.effectiveThreshold <= 0) return false
|
|
908
1100
|
if (this.storage?.supportsCaching === false) return false
|
|
1101
|
+
if (!this.storageSupportsTotal) return false
|
|
909
1102
|
return true
|
|
910
1103
|
}
|
|
911
1104
|
|
|
@@ -920,13 +1113,24 @@ export class EntityManager {
|
|
|
920
1113
|
|
|
921
1114
|
/**
|
|
922
1115
|
* Invalidate the cache (call after create/update/delete)
|
|
1116
|
+
*
|
|
1117
|
+
* Emits cache:entity:invalidated signal only when cache was previously valid
|
|
1118
|
+
* to avoid duplicate signals on repeated invalidation calls.
|
|
923
1119
|
*/
|
|
924
1120
|
invalidateCache() {
|
|
1121
|
+
const wasValid = this._cache.valid
|
|
1122
|
+
|
|
925
1123
|
this._cache.valid = false
|
|
926
1124
|
this._cache.items = []
|
|
927
1125
|
this._cache.total = 0
|
|
928
1126
|
this._cache.loadedAt = null
|
|
929
1127
|
this._cacheLoading = null
|
|
1128
|
+
|
|
1129
|
+
// Emit cache invalidation signal only when cache was actually valid
|
|
1130
|
+
// This prevents noise on repeated invalidation calls
|
|
1131
|
+
if (wasValid && this._signals) {
|
|
1132
|
+
this._signals.emit('cache:entity:invalidated', { entity: this.name })
|
|
1133
|
+
}
|
|
930
1134
|
}
|
|
931
1135
|
|
|
932
1136
|
/**
|
|
@@ -969,6 +1173,8 @@ export class EntityManager {
|
|
|
969
1173
|
this._cache.total = result.total
|
|
970
1174
|
this._cache.loadedAt = Date.now()
|
|
971
1175
|
this._cache.valid = true
|
|
1176
|
+
// Resolve parent fields for search (book.title, user.username, etc.)
|
|
1177
|
+
await this._resolveSearchFields(this._cache.items)
|
|
972
1178
|
return true
|
|
973
1179
|
}
|
|
974
1180
|
|
|
@@ -996,9 +1202,11 @@ export class EntityManager {
|
|
|
996
1202
|
|
|
997
1203
|
// If overflow or cache disabled, use API for accurate filtered results
|
|
998
1204
|
if (this.overflow || !this.isCacheEnabled) {
|
|
1205
|
+
console.log('[cache] API call for entity:', this.name, '(total > threshold)', 'isCacheEnabled:', this.isCacheEnabled, 'overflow:', this.overflow)
|
|
999
1206
|
result = await this.list(params)
|
|
1000
1207
|
} else {
|
|
1001
1208
|
// Full cache available - filter locally
|
|
1209
|
+
console.log('[cache] Using local cache for entity:', this.name)
|
|
1002
1210
|
const filtered = this._filterLocally(this._cache.items, params)
|
|
1003
1211
|
result = { ...filtered, fromCache: true }
|
|
1004
1212
|
}
|
|
@@ -1011,8 +1219,35 @@ export class EntityManager {
|
|
|
1011
1219
|
return result
|
|
1012
1220
|
}
|
|
1013
1221
|
|
|
1222
|
+
/**
|
|
1223
|
+
* Build MongoDB-like query from filters object
|
|
1224
|
+
*
|
|
1225
|
+
* Converts EntityManager filter params to QueryExecutor format:
|
|
1226
|
+
* - Skips null/undefined/empty string values
|
|
1227
|
+
* - Arrays become implicit $in (handled by QueryExecutor)
|
|
1228
|
+
* - Single values stay as implicit $eq
|
|
1229
|
+
*
|
|
1230
|
+
* @param {object} filters - Field filters { field: value }
|
|
1231
|
+
* @returns {object} - Query object for QueryExecutor
|
|
1232
|
+
* @private
|
|
1233
|
+
*/
|
|
1234
|
+
_buildQuery(filters) {
|
|
1235
|
+
const query = {}
|
|
1236
|
+
for (const [field, value] of Object.entries(filters)) {
|
|
1237
|
+
if (value === null || value === undefined || value === '') continue
|
|
1238
|
+
// Arrays and single values are passed through as-is
|
|
1239
|
+
// QueryExecutor handles implicit $in for arrays and $eq for primitives
|
|
1240
|
+
query[field] = value
|
|
1241
|
+
}
|
|
1242
|
+
return query
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1014
1245
|
/**
|
|
1015
1246
|
* Apply filters, search, sort and pagination locally
|
|
1247
|
+
*
|
|
1248
|
+
* Uses QueryExecutor for filtering (MongoDB-like operators supported).
|
|
1249
|
+
* Sort and pagination are applied after filtering.
|
|
1250
|
+
*
|
|
1016
1251
|
* @param {Array} items - All items
|
|
1017
1252
|
* @param {object} params - Query params
|
|
1018
1253
|
* @returns {{ items: Array, total: number }}
|
|
@@ -1020,6 +1255,7 @@ export class EntityManager {
|
|
|
1020
1255
|
_filterLocally(items, params = {}) {
|
|
1021
1256
|
const {
|
|
1022
1257
|
search = '',
|
|
1258
|
+
searchFields: overrideSearchFields = null, // Override storage's searchFields
|
|
1023
1259
|
filters = {},
|
|
1024
1260
|
sort_by = null,
|
|
1025
1261
|
sort_order = 'asc',
|
|
@@ -1029,40 +1265,72 @@ export class EntityManager {
|
|
|
1029
1265
|
|
|
1030
1266
|
let result = [...items]
|
|
1031
1267
|
|
|
1032
|
-
// Apply search
|
|
1268
|
+
// Apply search
|
|
1269
|
+
// If searchFields is declared, search in own fields + parent fields (from _search cache)
|
|
1270
|
+
// If not declared, search all string/number fields (backward compatible)
|
|
1271
|
+
// Override searchFields takes priority over storage's searchFields
|
|
1033
1272
|
if (search) {
|
|
1034
1273
|
const searchLower = search.toLowerCase()
|
|
1274
|
+
const searchFields = overrideSearchFields ?? this.storageSearchFields
|
|
1275
|
+
|
|
1035
1276
|
result = result.filter(item => {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1277
|
+
if (searchFields) {
|
|
1278
|
+
const { ownFields, parentFields } = this._parseSearchFields(searchFields)
|
|
1279
|
+
|
|
1280
|
+
// Search own fields
|
|
1281
|
+
for (const field of ownFields) {
|
|
1282
|
+
const value = item[field]
|
|
1283
|
+
if (typeof value === 'string' && value.toLowerCase().includes(searchLower)) {
|
|
1284
|
+
return true
|
|
1285
|
+
}
|
|
1286
|
+
if (typeof value === 'number' && value.toString().includes(search)) {
|
|
1287
|
+
return true
|
|
1288
|
+
}
|
|
1039
1289
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1290
|
+
|
|
1291
|
+
// Search cached parent fields (in item._search)
|
|
1292
|
+
if (item._search) {
|
|
1293
|
+
for (const [parentKey, fields] of Object.entries(parentFields)) {
|
|
1294
|
+
for (const field of fields) {
|
|
1295
|
+
const key = `${parentKey}.${field}`
|
|
1296
|
+
const value = item._search[key]
|
|
1297
|
+
if (typeof value === 'string' && value.toLowerCase().includes(searchLower)) {
|
|
1298
|
+
return true
|
|
1299
|
+
}
|
|
1300
|
+
if (typeof value === 'number' && value.toString().includes(search)) {
|
|
1301
|
+
return true
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1042
1305
|
}
|
|
1306
|
+
|
|
1043
1307
|
return false
|
|
1044
|
-
}
|
|
1308
|
+
} else {
|
|
1309
|
+
// No searchFields declared: search all string/number fields
|
|
1310
|
+
return Object.values(item).some(value => {
|
|
1311
|
+
if (typeof value === 'string') {
|
|
1312
|
+
return value.toLowerCase().includes(searchLower)
|
|
1313
|
+
}
|
|
1314
|
+
if (typeof value === 'number') {
|
|
1315
|
+
return value.toString().includes(search)
|
|
1316
|
+
}
|
|
1317
|
+
return false
|
|
1318
|
+
})
|
|
1319
|
+
}
|
|
1045
1320
|
})
|
|
1046
1321
|
}
|
|
1047
1322
|
|
|
1048
|
-
//
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
// Array filter (e.g., status in ['active', 'pending'])
|
|
1054
|
-
if (Array.isArray(value)) {
|
|
1055
|
-
return value.includes(itemValue)
|
|
1056
|
-
}
|
|
1057
|
-
// Exact match
|
|
1058
|
-
return itemValue === value
|
|
1059
|
-
})
|
|
1323
|
+
// Build query and apply filters using QueryExecutor
|
|
1324
|
+
const query = this._buildQuery(filters)
|
|
1325
|
+
if (Object.keys(query).length > 0) {
|
|
1326
|
+
const { items: filtered } = QueryExecutor.execute(result, query)
|
|
1327
|
+
result = filtered
|
|
1060
1328
|
}
|
|
1061
1329
|
|
|
1062
1330
|
// Total after filtering (before pagination)
|
|
1063
1331
|
const total = result.length
|
|
1064
1332
|
|
|
1065
|
-
// Apply sort
|
|
1333
|
+
// Apply sort (QueryExecutor does not sort)
|
|
1066
1334
|
if (sort_by) {
|
|
1067
1335
|
result.sort((a, b) => {
|
|
1068
1336
|
const aVal = a[sort_by]
|
|
@@ -1097,6 +1365,7 @@ export class EntityManager {
|
|
|
1097
1365
|
getCacheInfo() {
|
|
1098
1366
|
return {
|
|
1099
1367
|
enabled: this.isCacheEnabled,
|
|
1368
|
+
storageSupportsTotal: this.storageSupportsTotal,
|
|
1100
1369
|
threshold: this.effectiveThreshold,
|
|
1101
1370
|
valid: this._cache.valid,
|
|
1102
1371
|
overflow: this.overflow,
|
|
@@ -22,9 +22,26 @@
|
|
|
22
22
|
*/
|
|
23
23
|
export class ApiStorage {
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Storage capabilities declaration.
|
|
26
|
+
* Describes what features this storage adapter supports.
|
|
27
|
+
*
|
|
28
|
+
* @type {import('./index.js').StorageCapabilities}
|
|
26
29
|
*/
|
|
27
|
-
|
|
30
|
+
static capabilities = {
|
|
31
|
+
supportsTotal: true,
|
|
32
|
+
supportsFilters: true,
|
|
33
|
+
supportsPagination: true,
|
|
34
|
+
supportsCaching: true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Backward-compatible instance getter for supportsCaching.
|
|
39
|
+
* @deprecated Use static ApiStorage.capabilities.supportsCaching instead
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
get supportsCaching() {
|
|
43
|
+
return ApiStorage.capabilities.supportsCaching
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
constructor(options = {}) {
|
|
30
47
|
const {
|
|
@@ -19,9 +19,32 @@
|
|
|
19
19
|
*/
|
|
20
20
|
export class LocalStorage {
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Storage capabilities declaration.
|
|
23
|
+
* Describes what features this storage adapter supports.
|
|
24
|
+
*
|
|
25
|
+
* LocalStorage operates with browser localStorage:
|
|
26
|
+
* - supportsTotal: true - Returns accurate total from stored data
|
|
27
|
+
* - supportsFilters: true - Filters in-memory via list() params
|
|
28
|
+
* - supportsPagination: true - Paginates in-memory
|
|
29
|
+
* - supportsCaching: false - Already local, no cache benefit
|
|
30
|
+
*
|
|
31
|
+
* @type {import('./index.js').StorageCapabilities}
|
|
23
32
|
*/
|
|
24
|
-
|
|
33
|
+
static capabilities = {
|
|
34
|
+
supportsTotal: true,
|
|
35
|
+
supportsFilters: true,
|
|
36
|
+
supportsPagination: true,
|
|
37
|
+
supportsCaching: false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Backward-compatible instance getter for supportsCaching.
|
|
42
|
+
* @deprecated Use static LocalStorage.capabilities.supportsCaching instead
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
get supportsCaching() {
|
|
46
|
+
return LocalStorage.capabilities.supportsCaching
|
|
47
|
+
}
|
|
25
48
|
|
|
26
49
|
constructor(options = {}) {
|
|
27
50
|
const {
|
|
@@ -18,6 +18,34 @@
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export class MemoryStorage {
|
|
21
|
+
/**
|
|
22
|
+
* Storage capabilities declaration.
|
|
23
|
+
* Describes what features this storage adapter supports.
|
|
24
|
+
*
|
|
25
|
+
* MemoryStorage operates entirely in-memory:
|
|
26
|
+
* - supportsTotal: true - Returns accurate total from in-memory data
|
|
27
|
+
* - supportsFilters: true - Filters in-memory via list() params
|
|
28
|
+
* - supportsPagination: true - Paginates in-memory
|
|
29
|
+
* - supportsCaching: false - Already in-memory, no cache benefit
|
|
30
|
+
*
|
|
31
|
+
* @type {import('./index.js').StorageCapabilities}
|
|
32
|
+
*/
|
|
33
|
+
static capabilities = {
|
|
34
|
+
supportsTotal: true,
|
|
35
|
+
supportsFilters: true,
|
|
36
|
+
supportsPagination: true,
|
|
37
|
+
supportsCaching: false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Backward-compatible instance getter for supportsCaching.
|
|
42
|
+
* @deprecated Use static MemoryStorage.capabilities.supportsCaching instead
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
get supportsCaching() {
|
|
46
|
+
return MemoryStorage.capabilities.supportsCaching
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
constructor(options = {}) {
|
|
22
50
|
const {
|
|
23
51
|
idField = 'id',
|
|
@@ -20,9 +20,32 @@
|
|
|
20
20
|
*/
|
|
21
21
|
export class MockApiStorage {
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Storage capabilities declaration.
|
|
24
|
+
* Describes what features this storage adapter supports.
|
|
25
|
+
*
|
|
26
|
+
* MockApiStorage operates with in-memory Map + localStorage persistence:
|
|
27
|
+
* - supportsTotal: true - Returns accurate total from in-memory data
|
|
28
|
+
* - supportsFilters: true - Filters in-memory via list() params
|
|
29
|
+
* - supportsPagination: true - Paginates in-memory
|
|
30
|
+
* - supportsCaching: false - Already in-memory, no cache benefit
|
|
31
|
+
*
|
|
32
|
+
* @type {import('./index.js').StorageCapabilities}
|
|
24
33
|
*/
|
|
25
|
-
|
|
34
|
+
static capabilities = {
|
|
35
|
+
supportsTotal: true,
|
|
36
|
+
supportsFilters: true,
|
|
37
|
+
supportsPagination: true,
|
|
38
|
+
supportsCaching: false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Backward-compatible instance getter for supportsCaching.
|
|
43
|
+
* @deprecated Use static MockApiStorage.capabilities.supportsCaching instead
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
get supportsCaching() {
|
|
47
|
+
return MockApiStorage.capabilities.supportsCaching
|
|
48
|
+
}
|
|
26
49
|
|
|
27
50
|
constructor(options = {}) {
|
|
28
51
|
const {
|
|
@@ -95,9 +95,24 @@
|
|
|
95
95
|
*/
|
|
96
96
|
export class SdkStorage {
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* Storage capabilities declaration
|
|
99
|
+
* @type {import('./index.js').StorageCapabilities}
|
|
99
100
|
*/
|
|
100
|
-
|
|
101
|
+
static capabilities = {
|
|
102
|
+
supportsTotal: true, // list() returns { items, total }
|
|
103
|
+
supportsFilters: true, // list() accepts filters param
|
|
104
|
+
supportsPagination: true, // list() accepts page/page_size
|
|
105
|
+
supportsCaching: true // Benefits from EntityManager cache layer
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Backward-compat instance getter for supportsCaching
|
|
110
|
+
* @deprecated Use SdkStorage.capabilities.supportsCaching instead
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
get supportsCaching() {
|
|
114
|
+
return SdkStorage.capabilities.supportsCaching
|
|
115
|
+
}
|
|
101
116
|
|
|
102
117
|
/**
|
|
103
118
|
* @param {object} options
|