qdadm 0.17.0 → 0.25.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 +285 -46
- package/src/entity/EntityManager.js +338 -24
- package/src/entity/auth/AuthAdapter.js +59 -0
- package/src/entity/auth/RoleHierarchy.js +153 -0
- package/src/entity/auth/SecurityChecker.js +167 -0
- package/src/entity/auth/index.js +7 -0
- 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 +7 -0
- package/src/kernel/Kernel.js +73 -4
- package/src/module/moduleRegistry.js +31 -22
- package/src/query/FilterQuery.js +277 -0
- package/src/query/QueryExecutor.js +332 -0
- package/src/query/index.js +8 -0
- package/src/zones/ZoneRegistry.js +10 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoleHierarchy - Topological role resolution for Symfony-like permission system
|
|
3
|
+
*
|
|
4
|
+
* Roles form a Directed Acyclic Graph (DAG) where higher roles inherit
|
|
5
|
+
* permissions from lower roles. This class resolves the complete set of
|
|
6
|
+
* roles that a user effectively has based on their assigned roles.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* const hierarchy = new RoleHierarchy({
|
|
11
|
+
* ROLE_ADMIN: ['ROLE_USER'], // Admin inherits from User
|
|
12
|
+
* ROLE_SUPER_ADMIN: ['ROLE_ADMIN'], // Super admin inherits from Admin
|
|
13
|
+
* ROLE_MANAGER: ['ROLE_USER'], // Manager also inherits from User
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* hierarchy.getReachableRoles('ROLE_ADMIN')
|
|
17
|
+
* // Returns: ['ROLE_ADMIN', 'ROLE_USER']
|
|
18
|
+
*
|
|
19
|
+
* hierarchy.isGrantedRole(['ROLE_ADMIN'], 'ROLE_USER')
|
|
20
|
+
* // Returns: true (admin has user permissions)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export class RoleHierarchy {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object<string, string[]>} hierarchy - Role inheritance map
|
|
26
|
+
* Keys are role names, values are arrays of parent roles they inherit from
|
|
27
|
+
*/
|
|
28
|
+
constructor(hierarchy = {}) {
|
|
29
|
+
this.map = hierarchy
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve all roles reachable from a given role (topological traversal)
|
|
34
|
+
*
|
|
35
|
+
* Performs BFS traversal of the role graph to find all inherited roles.
|
|
36
|
+
* Handles cycles gracefully by tracking visited nodes.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} role - Starting role to resolve
|
|
39
|
+
* @returns {string[]} All roles including the starting role and all inherited roles
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // With hierarchy: { ROLE_ADMIN: ['ROLE_USER'] }
|
|
43
|
+
* hierarchy.getReachableRoles('ROLE_ADMIN')
|
|
44
|
+
* // Returns: ['ROLE_ADMIN', 'ROLE_USER']
|
|
45
|
+
*/
|
|
46
|
+
getReachableRoles(role) {
|
|
47
|
+
const visited = new Set()
|
|
48
|
+
const queue = [role]
|
|
49
|
+
|
|
50
|
+
while (queue.length > 0) {
|
|
51
|
+
const current = queue.shift()
|
|
52
|
+
if (visited.has(current)) continue
|
|
53
|
+
visited.add(current)
|
|
54
|
+
|
|
55
|
+
const parents = this.map[current] || []
|
|
56
|
+
queue.push(...parents)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return [...visited]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if user with given roles has the required role
|
|
64
|
+
*
|
|
65
|
+
* A user has a role if:
|
|
66
|
+
* 1. They are directly assigned that role, OR
|
|
67
|
+
* 2. They have a role that inherits from the required role
|
|
68
|
+
*
|
|
69
|
+
* @param {string|string[]} userRoles - Role(s) assigned to the user
|
|
70
|
+
* @param {string} requiredRole - The role to check for
|
|
71
|
+
* @returns {boolean} True if user has the required role (directly or inherited)
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // With hierarchy: { ROLE_ADMIN: ['ROLE_USER'] }
|
|
75
|
+
* hierarchy.isGrantedRole(['ROLE_ADMIN'], 'ROLE_USER') // true
|
|
76
|
+
* hierarchy.isGrantedRole(['ROLE_USER'], 'ROLE_ADMIN') // false
|
|
77
|
+
*/
|
|
78
|
+
isGrantedRole(userRoles, requiredRole) {
|
|
79
|
+
const roles = Array.isArray(userRoles) ? userRoles : [userRoles]
|
|
80
|
+
|
|
81
|
+
for (const role of roles) {
|
|
82
|
+
const reachable = this.getReachableRoles(role)
|
|
83
|
+
if (reachable.includes(requiredRole)) return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all roles that can reach a given role (reverse lookup)
|
|
91
|
+
*
|
|
92
|
+
* Useful for finding which roles would grant a specific permission.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} targetRole - The role to find grantors for
|
|
95
|
+
* @returns {string[]} All roles that have the target role in their reachable set
|
|
96
|
+
*/
|
|
97
|
+
getRolesGranting(targetRole) {
|
|
98
|
+
const grantors = []
|
|
99
|
+
|
|
100
|
+
for (const role of Object.keys(this.map)) {
|
|
101
|
+
if (this.isGrantedRole([role], targetRole)) {
|
|
102
|
+
grantors.push(role)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Also check the target role itself
|
|
107
|
+
if (!grantors.includes(targetRole)) {
|
|
108
|
+
grantors.push(targetRole)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return grantors
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate the hierarchy for cycles (optional sanity check)
|
|
116
|
+
*
|
|
117
|
+
* @returns {boolean} True if hierarchy is valid (no cycles)
|
|
118
|
+
*/
|
|
119
|
+
validate() {
|
|
120
|
+
const visiting = new Set()
|
|
121
|
+
const visited = new Set()
|
|
122
|
+
|
|
123
|
+
const hasCycle = (role) => {
|
|
124
|
+
if (visited.has(role)) return false
|
|
125
|
+
if (visiting.has(role)) return true
|
|
126
|
+
|
|
127
|
+
visiting.add(role)
|
|
128
|
+
const parents = this.map[role] || []
|
|
129
|
+
for (const parent of parents) {
|
|
130
|
+
if (hasCycle(parent)) return true
|
|
131
|
+
}
|
|
132
|
+
visiting.delete(role)
|
|
133
|
+
visited.add(role)
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const role of Object.keys(this.map)) {
|
|
138
|
+
if (hasCycle(role)) return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a RoleHierarchy instance from config
|
|
147
|
+
*
|
|
148
|
+
* @param {Object<string, string[]>} config - Role hierarchy configuration
|
|
149
|
+
* @returns {RoleHierarchy}
|
|
150
|
+
*/
|
|
151
|
+
export function createRoleHierarchy(config = {}) {
|
|
152
|
+
return new RoleHierarchy(config)
|
|
153
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { RoleHierarchy } from './RoleHierarchy.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SecurityChecker - Symfony-inspired permission checking
|
|
5
|
+
*
|
|
6
|
+
* Provides the `isGranted(attribute, subject?)` contract for checking permissions.
|
|
7
|
+
* Supports both role checks (ROLE_*) and permission checks (entity:action).
|
|
8
|
+
*
|
|
9
|
+
* Note: Symfony's full security system includes "voters" for custom business logic
|
|
10
|
+
* (e.g., "owner can edit their own posts"). This implementation focuses on
|
|
11
|
+
* role hierarchy + declarative permissions. For complex rules, override
|
|
12
|
+
* EntityManager.canRead/canWrite methods directly.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```js
|
|
16
|
+
* const checker = new SecurityChecker({
|
|
17
|
+
* roleHierarchy: new RoleHierarchy({ ROLE_ADMIN: ['ROLE_USER'] }),
|
|
18
|
+
* rolePermissions: {
|
|
19
|
+
* ROLE_USER: ['entity:read', 'entity:list'],
|
|
20
|
+
* ROLE_ADMIN: ['entity:create', 'entity:update', 'entity:delete'],
|
|
21
|
+
* },
|
|
22
|
+
* getCurrentUser: () => authStore.user
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* checker.isGranted('ROLE_ADMIN') // Check role
|
|
26
|
+
* checker.isGranted('entity:delete') // Check permission
|
|
27
|
+
* checker.isGranted('books:delete', book) // Check with subject
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class SecurityChecker {
|
|
31
|
+
/**
|
|
32
|
+
* @param {Object} options
|
|
33
|
+
* @param {RoleHierarchy} options.roleHierarchy - Role hierarchy instance
|
|
34
|
+
* @param {Object<string, string[]>} options.rolePermissions - Permissions per role
|
|
35
|
+
* @param {Function} options.getCurrentUser - Function returning current user or null
|
|
36
|
+
*/
|
|
37
|
+
constructor({ roleHierarchy, rolePermissions = {}, getCurrentUser }) {
|
|
38
|
+
this.roleHierarchy = roleHierarchy instanceof RoleHierarchy
|
|
39
|
+
? roleHierarchy
|
|
40
|
+
: new RoleHierarchy(roleHierarchy || {})
|
|
41
|
+
this.rolePermissions = rolePermissions
|
|
42
|
+
this.getCurrentUser = getCurrentUser
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if current user is granted an attribute (role or permission)
|
|
47
|
+
*
|
|
48
|
+
* This is the main contract method, similar to Symfony's isGranted().
|
|
49
|
+
*
|
|
50
|
+
* Checking flow:
|
|
51
|
+
* 1. If attribute starts with 'ROLE_' → check role hierarchy
|
|
52
|
+
* 2. Check if user has the permission (from role or direct)
|
|
53
|
+
*
|
|
54
|
+
* @param {string} attribute - Role (ROLE_*) or permission (entity:action)
|
|
55
|
+
* @param {object} [subject] - Optional subject for context-aware checks (reserved for future use)
|
|
56
|
+
* @returns {boolean} True if user is granted the attribute
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* checker.isGranted('ROLE_ADMIN') // true/false
|
|
60
|
+
* checker.isGranted('entity:delete') // true/false
|
|
61
|
+
* checker.isGranted('books:delete', book) // true/false (with subject)
|
|
62
|
+
*/
|
|
63
|
+
isGranted(attribute, subject = null) {
|
|
64
|
+
const user = this.getCurrentUser()
|
|
65
|
+
if (!user) return false
|
|
66
|
+
|
|
67
|
+
// 1. Check if it's a role (ROLE_*)
|
|
68
|
+
if (attribute.startsWith('ROLE_')) {
|
|
69
|
+
return this.roleHierarchy.isGrantedRole(
|
|
70
|
+
user.roles || [user.role],
|
|
71
|
+
attribute
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Check if it's a permission
|
|
76
|
+
const userPerms = this.getUserPermissions(user)
|
|
77
|
+
if (userPerms.includes('*')) return true
|
|
78
|
+
if (userPerms.includes(attribute)) return true
|
|
79
|
+
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all permissions for a user (resolved from roles + user overrides)
|
|
85
|
+
*
|
|
86
|
+
* Resolves permissions by:
|
|
87
|
+
* 1. Getting all reachable roles from role hierarchy
|
|
88
|
+
* 2. Collecting permissions from each role
|
|
89
|
+
* 3. Adding any user-specific permission overrides
|
|
90
|
+
*
|
|
91
|
+
* @param {object} user - User object with role/roles and optional permissions
|
|
92
|
+
* @returns {string[]} Array of all permissions
|
|
93
|
+
*/
|
|
94
|
+
getUserPermissions(user) {
|
|
95
|
+
const roles = user.roles || [user.role]
|
|
96
|
+
const perms = new Set()
|
|
97
|
+
|
|
98
|
+
for (const role of roles) {
|
|
99
|
+
if (!role) continue
|
|
100
|
+
const reachable = this.roleHierarchy.getReachableRoles(role)
|
|
101
|
+
for (const r of reachable) {
|
|
102
|
+
const rolePerms = this.rolePermissions[r] || []
|
|
103
|
+
rolePerms.forEach(p => perms.add(p))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// User-specific permission overrides
|
|
108
|
+
if (user.permissions && Array.isArray(user.permissions)) {
|
|
109
|
+
user.permissions.forEach(p => perms.add(p))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return [...perms]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if user can assign a role to another user
|
|
117
|
+
*
|
|
118
|
+
* Rule: Can only assign roles if user has 'role:assign' permission
|
|
119
|
+
* AND has the target role (or higher) themselves.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} targetRole - Role to assign
|
|
122
|
+
* @returns {boolean} True if user can assign this role
|
|
123
|
+
*/
|
|
124
|
+
canAssignRole(targetRole) {
|
|
125
|
+
return this.isGranted('role:assign') && this.isGranted(targetRole)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all roles that current user can assign
|
|
130
|
+
*
|
|
131
|
+
* @returns {string[]} Array of assignable role names
|
|
132
|
+
*/
|
|
133
|
+
getAssignableRoles() {
|
|
134
|
+
if (!this.isGranted('role:assign')) return []
|
|
135
|
+
|
|
136
|
+
const user = this.getCurrentUser()
|
|
137
|
+
if (!user) return []
|
|
138
|
+
|
|
139
|
+
const userRoles = user.roles || [user.role]
|
|
140
|
+
const assignable = new Set()
|
|
141
|
+
|
|
142
|
+
for (const role of userRoles) {
|
|
143
|
+
if (!role) continue
|
|
144
|
+
const reachable = this.roleHierarchy.getReachableRoles(role)
|
|
145
|
+
reachable.forEach(r => assignable.add(r))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [...assignable]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a SecurityChecker instance from config
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} config
|
|
156
|
+
* @param {Object<string, string[]>} config.role_hierarchy - Role hierarchy config
|
|
157
|
+
* @param {Object<string, string[]>} config.role_permissions - Permissions per role
|
|
158
|
+
* @param {Function} config.getCurrentUser - User getter function
|
|
159
|
+
* @returns {SecurityChecker}
|
|
160
|
+
*/
|
|
161
|
+
export function createSecurityChecker(config) {
|
|
162
|
+
return new SecurityChecker({
|
|
163
|
+
roleHierarchy: new RoleHierarchy(config.role_hierarchy || {}),
|
|
164
|
+
rolePermissions: config.role_permissions || {},
|
|
165
|
+
getCurrentUser: config.getCurrentUser
|
|
166
|
+
})
|
|
167
|
+
}
|
package/src/entity/auth/index.js
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
* Auth Module
|
|
3
3
|
*
|
|
4
4
|
* AuthAdapter interface and implementations for scope/silo permission checks.
|
|
5
|
+
* Includes Symfony-inspired role hierarchy and security checker.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
// Interface
|
|
8
9
|
export { AuthAdapter, AuthActions } from './AuthAdapter.js'
|
|
9
10
|
|
|
11
|
+
// Role Hierarchy (topological resolution)
|
|
12
|
+
export { RoleHierarchy, createRoleHierarchy } from './RoleHierarchy.js'
|
|
13
|
+
|
|
14
|
+
// Security Checker (isGranted contract)
|
|
15
|
+
export { SecurityChecker, createSecurityChecker } from './SecurityChecker.js'
|
|
16
|
+
|
|
10
17
|
// Implementations
|
|
11
18
|
export { PermissiveAuthAdapter, createPermissiveAdapter } from './PermissiveAdapter.js'
|
|
@@ -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
|
|
@@ -5,6 +5,111 @@
|
|
|
5
5
|
* EntityManagers can use these or implement their own storage.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Storage Capabilities Interface
|
|
10
|
+
*
|
|
11
|
+
* Static capabilities declaration that storage adapters expose via `static capabilities = {...}`.
|
|
12
|
+
* EntityManager reads these via `storage.constructor.capabilities` to determine feature support.
|
|
13
|
+
*
|
|
14
|
+
* All properties default to `false` if not declared (conservative assumption).
|
|
15
|
+
* Custom storage implementations that don't declare capabilities will degrade gracefully
|
|
16
|
+
* via fallback to empty `{}`.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {object} StorageCapabilities
|
|
19
|
+
* @property {boolean} [supportsTotal=false] - Storage `list()` returns `{ items, total }` with accurate total count
|
|
20
|
+
* @property {boolean} [supportsFilters=false] - Storage `list()` accepts `filters` param and handles filtering
|
|
21
|
+
* @property {boolean} [supportsPagination=false] - Storage `list()` accepts `page`/`page_size` params
|
|
22
|
+
* @property {boolean} [supportsCaching=false] - Storage benefits from EntityManager cache layer (network-based storages)
|
|
23
|
+
* @property {string[]} [searchFields] - Fields to search when filtering locally. Supports own fields ('title')
|
|
24
|
+
* and parent entity fields ('book.title') via EntityManager.parents config. When undefined, all string
|
|
25
|
+
* fields are searched (default behavior). When defined, only listed fields are searched.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Migration Guide for Custom Storage Adapters
|
|
30
|
+
*
|
|
31
|
+
* If you have a custom storage adapter, you can add capabilities support in two ways:
|
|
32
|
+
*
|
|
33
|
+
* **Option 1: Static capabilities (recommended)**
|
|
34
|
+
* Add a static `capabilities` property to your class. EntityManager reads this via
|
|
35
|
+
* `storage.constructor.capabilities`. This is the preferred pattern for new code.
|
|
36
|
+
*
|
|
37
|
+
* ```js
|
|
38
|
+
* export class MyCustomStorage {
|
|
39
|
+
* static capabilities = {
|
|
40
|
+
* supportsTotal: true,
|
|
41
|
+
* supportsFilters: true,
|
|
42
|
+
* supportsPagination: true,
|
|
43
|
+
* supportsCaching: true // set to false for in-memory storages
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* // Backward-compat instance getter (optional, for smooth migration)
|
|
47
|
+
* get supportsCaching() {
|
|
48
|
+
* return MyCustomStorage.capabilities.supportsCaching
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* async list(params) { ... }
|
|
52
|
+
* async get(id) { ... }
|
|
53
|
+
* // ... other methods
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* **Option 2: Instance property only (legacy)**
|
|
58
|
+
* Keep using instance properties. EntityManager's `isCacheEnabled` check uses:
|
|
59
|
+
* `if (storage?.supportsCaching === false) return false`
|
|
60
|
+
* This works with instance properties directly.
|
|
61
|
+
*
|
|
62
|
+
* ```js
|
|
63
|
+
* export class MyLegacyStorage {
|
|
64
|
+
* supportsCaching = true // or false for in-memory
|
|
65
|
+
*
|
|
66
|
+
* async list(params) { ... }
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* **No capabilities declared**
|
|
71
|
+
* If your storage doesn't declare capabilities, EntityManager will:
|
|
72
|
+
* - Assume `supportsTotal: false` (disables auto-caching threshold check)
|
|
73
|
+
* - Assume `supportsCaching: undefined` (caching allowed, but threshold check fails)
|
|
74
|
+
* - Gracefully degrade without errors
|
|
75
|
+
*
|
|
76
|
+
* Use `getStorageCapabilities(storage)` helper to read merged capabilities with defaults.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Default capabilities for storages that don't declare their own.
|
|
81
|
+
* All capabilities default to false for safe degradation.
|
|
82
|
+
*
|
|
83
|
+
* @type {StorageCapabilities}
|
|
84
|
+
*/
|
|
85
|
+
export const DEFAULT_STORAGE_CAPABILITIES = {
|
|
86
|
+
supportsTotal: false,
|
|
87
|
+
supportsFilters: false,
|
|
88
|
+
supportsPagination: false,
|
|
89
|
+
supportsCaching: false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read capabilities from a storage instance.
|
|
94
|
+
* Accesses static `capabilities` property via constructor with fallback to defaults.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} storage - Storage adapter instance
|
|
97
|
+
* @returns {StorageCapabilities} Merged capabilities with defaults
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* const caps = getStorageCapabilities(myStorage)
|
|
101
|
+
* if (caps.supportsTotal) {
|
|
102
|
+
* // Storage provides accurate total count
|
|
103
|
+
* }
|
|
104
|
+
*/
|
|
105
|
+
export function getStorageCapabilities(storage) {
|
|
106
|
+
const declared = storage?.constructor?.capabilities || {}
|
|
107
|
+
return {
|
|
108
|
+
...DEFAULT_STORAGE_CAPABILITIES,
|
|
109
|
+
...declared
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
8
113
|
export { ApiStorage, createApiStorage } from './ApiStorage.js'
|
|
9
114
|
export { LocalStorage, createLocalStorage } from './LocalStorage.js'
|
|
10
115
|
export { MemoryStorage, createMemoryStorage } from './MemoryStorage.js'
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* A framework for building admin dashboards with Vue 3, PrimeVue, and Vue Router.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
// Version (from package.json)
|
|
8
|
+
import pkg from '../package.json'
|
|
9
|
+
export const version = pkg.version
|
|
10
|
+
|
|
7
11
|
// Kernel (simplified bootstrap)
|
|
8
12
|
export * from './kernel/index.js'
|
|
9
13
|
|
|
@@ -34,6 +38,9 @@ export * from './hooks/index.js'
|
|
|
34
38
|
// Core (extension helpers)
|
|
35
39
|
export * from './core/index.js'
|
|
36
40
|
|
|
41
|
+
// Query (MongoDB-like filtering)
|
|
42
|
+
export * from './query/index.js'
|
|
43
|
+
|
|
37
44
|
// Utils
|
|
38
45
|
export * from './utils/index.js'
|
|
39
46
|
|