@svadmin/core 0.14.0 → 0.16.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/auth-hooks.svelte.ts +6 -3
- package/src/context.svelte.ts +1 -1
- package/src/form-hooks.svelte.ts +3 -3
- package/src/hook-utils.svelte.ts +1 -0
- package/src/i18n.svelte.ts +104 -0
- package/src/index.ts +2 -2
- package/src/options.svelte.ts +2 -0
- package/src/permissions.test.ts +1 -7
- package/src/permissions.ts +1 -11
- package/src/query-hooks.svelte.ts +1 -1
- package/src/router.ts +2 -2
- package/src/routing-hooks.svelte.ts +0 -1
- package/src/table-hooks.svelte.ts +5 -5
- package/src/types.ts +32 -2
- package/src/useCan.ts +12 -20
package/package.json
CHANGED
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -181,7 +181,9 @@ export function useGetIdentity() {
|
|
|
181
181
|
let isLoading = $state(true);
|
|
182
182
|
let error = $state<Error | null>(null);
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
function fetch() {
|
|
185
|
+
if (!provider) { isLoading = false; return; }
|
|
186
|
+
isLoading = true;
|
|
185
187
|
provider.getIdentity().then(identity => {
|
|
186
188
|
data = identity;
|
|
187
189
|
isLoading = false;
|
|
@@ -190,14 +192,15 @@ export function useGetIdentity() {
|
|
|
190
192
|
isLoading = false;
|
|
191
193
|
console.warn('[svadmin] useGetIdentity failed:', err);
|
|
192
194
|
});
|
|
193
|
-
} else {
|
|
194
|
-
isLoading = false;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
fetch();
|
|
198
|
+
|
|
197
199
|
return {
|
|
198
200
|
get data() { return data; },
|
|
199
201
|
get isLoading() { return isLoading; },
|
|
200
202
|
get error() { return error; },
|
|
203
|
+
refetch: fetch,
|
|
201
204
|
};
|
|
202
205
|
}
|
|
203
206
|
|
package/src/context.svelte.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { RouterProvider } from './router-provider';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Accepts either:
|
|
11
|
-
* - A single DataProvider
|
|
11
|
+
* - A single DataProvider
|
|
12
12
|
* - A record of named providers: { default: rest, cms: graphql }
|
|
13
13
|
*/
|
|
14
14
|
export type DataProviderInput = DataProvider | Record<string, DataProvider>;
|
package/src/form-hooks.svelte.ts
CHANGED
|
@@ -125,8 +125,8 @@ export interface UseFormReturn<
|
|
|
125
125
|
readonly autoSave: { status: 'idle' | 'saving' | 'saved' | 'error'; data: unknown; error: unknown };
|
|
126
126
|
|
|
127
127
|
// ─── Raw query/mutation (escape hatch) ────────────────────────
|
|
128
|
-
readonly query:
|
|
129
|
-
readonly mutation:
|
|
128
|
+
readonly query: unknown;
|
|
129
|
+
readonly mutation: unknown;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// ─── Implementation ──────────────────────────────────────────────────
|
|
@@ -423,6 +423,6 @@ export function useForm<
|
|
|
423
423
|
|
|
424
424
|
// Raw escape hatches
|
|
425
425
|
query,
|
|
426
|
-
mutation:
|
|
426
|
+
mutation: action === 'edit' ? updateMut : createMut,
|
|
427
427
|
};
|
|
428
428
|
}
|
package/src/hook-utils.svelte.ts
CHANGED
|
@@ -97,6 +97,7 @@ export function fireSuccessNotification(
|
|
|
97
97
|
resource?: string,
|
|
98
98
|
): void {
|
|
99
99
|
if (config === false) return;
|
|
100
|
+
if (!config && !defaultMessage) return;
|
|
100
101
|
if (typeof config === 'function') {
|
|
101
102
|
const result = config(data, values, resource);
|
|
102
103
|
toast.success(result.message);
|
package/src/i18n.svelte.ts
CHANGED
|
@@ -140,6 +140,58 @@ const locales: Record<string, Locale> = {
|
|
|
140
140
|
'validation.maxLength': '最多 {max} 个字符',
|
|
141
141
|
'validation.invalidEmail': '请输入有效的邮箱地址',
|
|
142
142
|
'validation.invalidFormat': '格式不正确',
|
|
143
|
+
// Settings
|
|
144
|
+
'settings.title': '设置',
|
|
145
|
+
'settings.profile': '个人资料',
|
|
146
|
+
'settings.appearance': '外观设置',
|
|
147
|
+
'settings.about': '关于',
|
|
148
|
+
'settings.general': '通用',
|
|
149
|
+
'settings.system': '系统',
|
|
150
|
+
'settings.themeMode': '主题模式',
|
|
151
|
+
'settings.light': '浅色',
|
|
152
|
+
'settings.dark': '深色',
|
|
153
|
+
'settings.systemTheme': '跟随系统',
|
|
154
|
+
'settings.colorAccent': '强调色',
|
|
155
|
+
'settings.language': '语言',
|
|
156
|
+
'settings.interface': '界面',
|
|
157
|
+
'settings.sidebarDensity': '侧边栏密度',
|
|
158
|
+
'settings.compact': '紧凑',
|
|
159
|
+
'settings.standard': '标准',
|
|
160
|
+
'settings.defaultPageSize': '默认每页条数',
|
|
161
|
+
'settings.version': '版本',
|
|
162
|
+
'settings.registeredResources': '已注册资源',
|
|
163
|
+
'settings.dataProvider': '数据源',
|
|
164
|
+
'settings.currentEnvironment': '当前环境',
|
|
165
|
+
'settings.settingsDescription': '管理您的账户和界面偏好。',
|
|
166
|
+
'settings.rolesAndPermissions': '角色与权限',
|
|
167
|
+
'settings.roles': '角色',
|
|
168
|
+
'settings.rolesDescription': '管理系统访问控制与资源权限。',
|
|
169
|
+
'settings.rbacNotSupported': '当前数据源不支持 RBAC 管理',
|
|
170
|
+
'settings.selectRoleHint': '请在左侧选择一个角色以查看权限',
|
|
171
|
+
'settings.resource': '资源',
|
|
172
|
+
'resource.posts': '文章',
|
|
173
|
+
'resource.users': '用户',
|
|
174
|
+
'resource.categories': '分类',
|
|
175
|
+
// Audit
|
|
176
|
+
'settings.auditLogs': '操作日志',
|
|
177
|
+
'settings.auditDescription': '追踪和审查所有系统操作,用于合规与调试。',
|
|
178
|
+
'settings.auditNotSupported': '当前数据源不支持操作日志',
|
|
179
|
+
'settings.searchLogs': '搜索用户、操作或资源...',
|
|
180
|
+
'settings.auditTime': '时间',
|
|
181
|
+
'settings.auditUser': '操作人',
|
|
182
|
+
'settings.auditAction': '操作',
|
|
183
|
+
'settings.auditResource': '资源',
|
|
184
|
+
'settings.auditIp': 'IP 地址',
|
|
185
|
+
'settings.auditTotal': '条记录',
|
|
186
|
+
'settings.auditSnapshot': '变更快照',
|
|
187
|
+
// Profile
|
|
188
|
+
'profile.title': '个人资料',
|
|
189
|
+
'profile.name': '姓名',
|
|
190
|
+
'profile.notAvailable': '无法获取资料',
|
|
191
|
+
'profile.changePassword': '修改密码',
|
|
192
|
+
'profile.currentPassword': '当前密码',
|
|
193
|
+
'profile.passwordChanged': '密码已修改',
|
|
194
|
+
'profile.updatePassword': '更新密码',
|
|
143
195
|
},
|
|
144
196
|
'en': {
|
|
145
197
|
// Common
|
|
@@ -278,6 +330,58 @@ const locales: Record<string, Locale> = {
|
|
|
278
330
|
'validation.maxLength': 'Maximum {max} characters',
|
|
279
331
|
'validation.invalidEmail': 'Please enter a valid email address',
|
|
280
332
|
'validation.invalidFormat': 'Invalid format',
|
|
333
|
+
// Settings
|
|
334
|
+
'settings.title': 'Settings',
|
|
335
|
+
'settings.profile': 'Profile',
|
|
336
|
+
'settings.appearance': 'Appearance',
|
|
337
|
+
'settings.about': 'About',
|
|
338
|
+
'settings.general': 'General',
|
|
339
|
+
'settings.system': 'System',
|
|
340
|
+
'settings.themeMode': 'Theme Mode',
|
|
341
|
+
'settings.light': 'Light',
|
|
342
|
+
'settings.dark': 'Dark',
|
|
343
|
+
'settings.systemTheme': 'System',
|
|
344
|
+
'settings.colorAccent': 'Color Accent',
|
|
345
|
+
'settings.language': 'Language',
|
|
346
|
+
'settings.interface': 'Interface',
|
|
347
|
+
'settings.sidebarDensity': 'Sidebar Density',
|
|
348
|
+
'settings.compact': 'Compact',
|
|
349
|
+
'settings.standard': 'Standard',
|
|
350
|
+
'settings.defaultPageSize': 'Default Page Size',
|
|
351
|
+
'settings.version': 'Version',
|
|
352
|
+
'settings.registeredResources': 'Registered Resources',
|
|
353
|
+
'settings.dataProvider': 'Data Provider',
|
|
354
|
+
'settings.currentEnvironment': 'Current Environment',
|
|
355
|
+
'settings.settingsDescription': 'Manage your account and interface preferences.',
|
|
356
|
+
'settings.rolesAndPermissions': 'Roles & Permissions',
|
|
357
|
+
'settings.roles': 'Roles',
|
|
358
|
+
'settings.rolesDescription': 'Manage system access control and resource permissions.',
|
|
359
|
+
'settings.rbacNotSupported': 'RBAC management is not supported by the current data provider',
|
|
360
|
+
'settings.selectRoleHint': 'Select a role on the left to view permissions',
|
|
361
|
+
'settings.resource': 'Resource',
|
|
362
|
+
'resource.posts': 'Posts',
|
|
363
|
+
'resource.users': 'Users',
|
|
364
|
+
'resource.categories': 'Categories',
|
|
365
|
+
// Audit
|
|
366
|
+
'settings.auditLogs': 'Audit Logs',
|
|
367
|
+
'settings.auditDescription': 'Track and review all system operations for compliance and debugging.',
|
|
368
|
+
'settings.auditNotSupported': 'Audit logs are not supported by the current data provider',
|
|
369
|
+
'settings.searchLogs': 'Search by user, action, or resource...',
|
|
370
|
+
'settings.auditTime': 'Time',
|
|
371
|
+
'settings.auditUser': 'User',
|
|
372
|
+
'settings.auditAction': 'Action',
|
|
373
|
+
'settings.auditResource': 'Resource',
|
|
374
|
+
'settings.auditIp': 'IP Address',
|
|
375
|
+
'settings.auditTotal': 'total records',
|
|
376
|
+
'settings.auditSnapshot': 'Change Snapshot',
|
|
377
|
+
// Profile
|
|
378
|
+
'profile.title': 'Profile',
|
|
379
|
+
'profile.name': 'Name',
|
|
380
|
+
'profile.notAvailable': 'Profile not available',
|
|
381
|
+
'profile.changePassword': 'Change Password',
|
|
382
|
+
'profile.currentPassword': 'Current Password',
|
|
383
|
+
'profile.passwordChanged': 'Password updated successfully',
|
|
384
|
+
'profile.updatePassword': 'Update Password',
|
|
281
385
|
},
|
|
282
386
|
};
|
|
283
387
|
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export {
|
|
|
27
27
|
} from './hooks.svelte';
|
|
28
28
|
export { matchRoute, navigate, currentPath, setActiveRouterProvider } from './router';
|
|
29
29
|
export { readURLState, writeURLState } from './url-sync';
|
|
30
|
-
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions,
|
|
30
|
+
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync } from './permissions';
|
|
31
31
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
32
32
|
export { toast } from './toast.svelte';
|
|
33
33
|
export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
|
|
@@ -61,7 +61,7 @@ export type {
|
|
|
61
61
|
ResourceDefinition, FieldDefinition, MenuItem,
|
|
62
62
|
AuthActionResult, CheckResult,
|
|
63
63
|
ResourceTypeMap, KnownResources, InferData,
|
|
64
|
-
BaseRecord,
|
|
64
|
+
BaseRecord, Role, AuditLog
|
|
65
65
|
} from './types';
|
|
66
66
|
export type { InvalidateScope } from './options.svelte';
|
|
67
67
|
export type { LiveProvider, LiveEvent, LiveMode } from './live.svelte';
|
package/src/options.svelte.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface AdminOptions {
|
|
|
44
44
|
disableRouteChangeHandler?: boolean;
|
|
45
45
|
onLiveEvent?: (event: unknown) => void;
|
|
46
46
|
menuItems?: (items: { name: string; route: string; label: string; icon?: string; parentName?: string; order?: number }[]) => { name: string; route: string; label: string; icon?: string; parentName?: string; order?: number }[];
|
|
47
|
+
/** Default page size for tables. Falls back to localStorage 'svadmin-default-page-size' or 10. */
|
|
48
|
+
defaultPageSize?: number;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const defaultTextTransformers: TextTransformers = {
|
package/src/permissions.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Unit tests for permissions module
|
|
2
2
|
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
3
|
-
import { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions,
|
|
3
|
+
import { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync } from './permissions';
|
|
4
4
|
import type { Action, AccessControlProvider } from './permissions';
|
|
5
5
|
|
|
6
6
|
describe('permissions', () => {
|
|
@@ -11,12 +11,6 @@ describe('permissions', () => {
|
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
test('canAccess returns { can: true } when provider is async (warns)', () => {
|
|
15
|
-
// canAccess is sync but provider.can() is async — should default to { can: true }
|
|
16
|
-
const result = canAccess('posts', 'list' as Action);
|
|
17
|
-
expect(result.can).toBe(true);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
14
|
test('canAccessAsync respects deny rule', async () => {
|
|
21
15
|
setAccessControlProvider({
|
|
22
16
|
can: async ({ action }) => {
|
package/src/permissions.ts
CHANGED
|
@@ -73,17 +73,7 @@ export function getAccessControlOptions() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
77
|
-
* When a provider is registered, this warns since provider.can() is always async.
|
|
78
|
-
*/
|
|
79
|
-
export function canAccess(resource: string, action: Action, params?: Record<string, unknown>): CanResult {
|
|
80
|
-
if (!provider) return { can: true };
|
|
81
|
-
console.warn('[permissions] canAccess called synchronously but provider.can() is async. Defaulting to { can: true }. Use canAccessAsync() instead.');
|
|
82
|
-
return { can: true };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Async access check. Preferred over `canAccess()`.
|
|
76
|
+
* Async access check. Returns `{ can: true }` if no provider is registered.
|
|
87
77
|
*/
|
|
88
78
|
export async function canAccessAsync(resource: string, action: Action, params?: Record<string, unknown>): Promise<CanResult> {
|
|
89
79
|
if (!provider) return { can: true };
|
|
@@ -75,7 +75,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
$effect(() => {
|
|
78
|
-
if (query.isSuccess) {
|
|
78
|
+
if (query.isSuccess && options.successNotification) {
|
|
79
79
|
fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
|
|
80
80
|
} else if (query.isError) {
|
|
81
81
|
fireErrorNotification(options.errorNotification, 'Fetch failed', query.error);
|
package/src/router.ts
CHANGED
|
@@ -53,9 +53,9 @@ export function matchRoute(
|
|
|
53
53
|
/**
|
|
54
54
|
* Navigate to a path. Uses RouterProvider if available, falls back to hash.
|
|
55
55
|
*/
|
|
56
|
-
export function navigate(path: string): void {
|
|
56
|
+
export function navigate(path: string, options?: { replaceState?: boolean }): void {
|
|
57
57
|
if (_routerProvider) {
|
|
58
|
-
_routerProvider.go({ to: path });
|
|
58
|
+
_routerProvider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
|
|
59
59
|
} else {
|
|
60
60
|
window.location.hash = '#' + path.replace(/^#/, '');
|
|
61
61
|
}
|
|
@@ -42,7 +42,6 @@ export function useGo() {
|
|
|
42
42
|
if (routerProvider?.go) {
|
|
43
43
|
routerProvider.go({ to: targetUrl, type: options.type });
|
|
44
44
|
} else {
|
|
45
|
-
// @ts-expect-error SvelteKit / internal navigate might only take 1 arg or options obj
|
|
46
45
|
navigate(targetUrl, options.type === 'replace' ? { replaceState: true } : undefined);
|
|
47
46
|
}
|
|
48
47
|
};
|
|
@@ -69,15 +69,15 @@ export function useTable<
|
|
|
69
69
|
|
|
70
70
|
const querySorters = $derived(sortersMode === 'server' ? effectiveSorters : []);
|
|
71
71
|
const queryFilters = $derived(filtersMode === 'server' ? effectiveFilters : []);
|
|
72
|
-
const queryPagination = paginationMode === 'server'
|
|
72
|
+
const queryPagination = $derived(paginationMode === 'server'
|
|
73
73
|
? pagination
|
|
74
|
-
: { current: 1, pageSize: 999999, mode: paginationMode as 'client' | 'off' };
|
|
74
|
+
: { current: 1, pageSize: 999999, mode: paginationMode as 'client' | 'off' });
|
|
75
75
|
|
|
76
76
|
const tableQueryInfo = useList<TQueryFnData, TError>({
|
|
77
77
|
resource,
|
|
78
|
-
pagination
|
|
79
|
-
sorters
|
|
80
|
-
filters
|
|
78
|
+
get pagination() { return queryPagination; },
|
|
79
|
+
get sorters() { return querySorters; },
|
|
80
|
+
get filters() { return queryFilters; },
|
|
81
81
|
meta,
|
|
82
82
|
dataProviderName,
|
|
83
83
|
...restOptions
|
package/src/types.ts
CHANGED
|
@@ -213,6 +213,13 @@ export interface CheckResult {
|
|
|
213
213
|
logout?: boolean;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
export interface Role {
|
|
217
|
+
id: string;
|
|
218
|
+
name: string;
|
|
219
|
+
description?: string;
|
|
220
|
+
[key: string]: unknown;
|
|
221
|
+
}
|
|
222
|
+
|
|
216
223
|
export interface AuthProvider {
|
|
217
224
|
login: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
218
225
|
logout: (params?: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
@@ -222,9 +229,30 @@ export interface AuthProvider {
|
|
|
222
229
|
register?: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
223
230
|
forgotPassword?: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
224
231
|
updatePassword?: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
232
|
+
/** Update user profile (name, avatar, etc.) */
|
|
233
|
+
updateProfile?: (params: { name?: string; avatar?: string | File; [key: string]: unknown }) => Promise<AuthActionResult>;
|
|
234
|
+
/** Get list of system roles for RBAC mapping */
|
|
235
|
+
getRoles?: () => Promise<Role[]>;
|
|
236
|
+
/** Get specific role permissions as a matrix map, e.g. { "users": ["create", "read"] } */
|
|
237
|
+
getRolePermissions?: (roleId: string) => Promise<Record<string, string[]>>;
|
|
238
|
+
/** Update permissions for a specific role */
|
|
239
|
+
updateRolePermissions?: (roleId: string, permissions: Record<string, string[]>) => Promise<AuthActionResult>;
|
|
240
|
+
/** Get recent audit logs */
|
|
241
|
+
getAuditLogs?: (params?: { page?: number; pageSize?: number }) => Promise<{ data: AuditLog[]; total: number }>;
|
|
225
242
|
onError?: (error: unknown) => Promise<{ redirectTo?: string; logout?: boolean }>;
|
|
226
243
|
}
|
|
227
244
|
|
|
245
|
+
export interface AuditLog {
|
|
246
|
+
id: string | number;
|
|
247
|
+
userId?: string | number;
|
|
248
|
+
userName?: string;
|
|
249
|
+
action: string;
|
|
250
|
+
resource?: string;
|
|
251
|
+
createdAt: string | Date;
|
|
252
|
+
ipAddress?: string;
|
|
253
|
+
details?: Record<string, unknown> | string;
|
|
254
|
+
}
|
|
255
|
+
|
|
228
256
|
// ─── NotificationProvider ─────────────────────────────────────
|
|
229
257
|
|
|
230
258
|
export interface NotificationProvider {
|
|
@@ -289,7 +317,7 @@ export interface FieldDefinition {
|
|
|
289
317
|
label: string;
|
|
290
318
|
type: 'text' | 'number' | 'boolean' | 'date' | 'select' | 'multiselect' | 'tags'
|
|
291
319
|
| 'textarea' | 'richtext' | 'image' | 'images' | 'json' | 'relation' | 'color'
|
|
292
|
-
| 'url' | 'email' | 'phone' | 'file' | 'markdown' | 'password';
|
|
320
|
+
| 'url' | 'email' | 'phone' | 'file' | 'markdown' | 'password' | 'array';
|
|
293
321
|
required?: boolean;
|
|
294
322
|
searchable?: boolean;
|
|
295
323
|
sortable?: boolean;
|
|
@@ -306,6 +334,8 @@ export interface FieldDefinition {
|
|
|
306
334
|
resource?: string; // related resource name
|
|
307
335
|
optionLabel?: string; // field to use as label
|
|
308
336
|
optionValue?: string; // field to use as value
|
|
337
|
+
// Array support
|
|
338
|
+
subFields?: FieldDefinition[]; // nested fields for array types
|
|
309
339
|
// Validation
|
|
310
340
|
validate?: (value: unknown) => string | null;
|
|
311
341
|
// Grouping
|
|
@@ -334,7 +364,7 @@ export interface FieldDefinition {
|
|
|
334
364
|
*/
|
|
335
365
|
export interface ResourceTypeMap {}
|
|
336
366
|
|
|
337
|
-
/** When ResourceTypeMap is empty → string
|
|
367
|
+
/** When ResourceTypeMap is empty → string; otherwise → registered keys */
|
|
338
368
|
export type KnownResources = keyof ResourceTypeMap extends never
|
|
339
369
|
? string
|
|
340
370
|
: Extract<keyof ResourceTypeMap, string>
|
package/src/useCan.ts
CHANGED
|
@@ -22,36 +22,28 @@ export interface UseCanResult {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Reactive permission check backed by TanStack Query for caching and deduplication.
|
|
25
|
+
* Accepts a getter function for Svelte 5 fine-grained reactivity.
|
|
25
26
|
*
|
|
26
27
|
* @example
|
|
27
28
|
* ```ts
|
|
28
|
-
*
|
|
29
|
-
* const can = useCan('posts', 'delete');
|
|
29
|
+
* const can = useCan(() => ({ resource: 'posts', action: 'delete', params: { id: 1 } }));
|
|
30
30
|
* if (can.allowed) { ... }
|
|
31
|
-
*
|
|
32
|
-
* // With options object
|
|
33
|
-
* const can = useCan({ resource: 'posts', action: 'delete', params: { id: 1 } });
|
|
34
31
|
* ```
|
|
35
32
|
*/
|
|
36
|
-
export function useCan(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
enabled: options.queryOptions?.enabled ?? true,
|
|
47
|
-
staleTime: options.queryOptions?.staleTime ?? 5 * 60 * 1000, // 5 min default cache
|
|
48
|
-
}));
|
|
33
|
+
export function useCan(options: () => UseCanOptions): UseCanResult {
|
|
34
|
+
const query = createQuery<CanResult>(() => {
|
|
35
|
+
const opts = options();
|
|
36
|
+
return {
|
|
37
|
+
queryKey: ['useCan', opts.resource, opts.action, opts.params] as const,
|
|
38
|
+
queryFn: () => canAccessAsync(opts.resource, opts.action, opts.params),
|
|
39
|
+
enabled: opts.queryOptions?.enabled ?? true,
|
|
40
|
+
staleTime: opts.queryOptions?.staleTime ?? 5 * 60 * 1000,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
49
43
|
|
|
50
|
-
// Access via query store — use safe property access with fallbacks
|
|
51
44
|
return {
|
|
52
45
|
get allowed() { return (query.data as CanResult | undefined)?.can ?? true; },
|
|
53
46
|
get reason() { return (query.data as CanResult | undefined)?.reason; },
|
|
54
47
|
get isLoading() { return query.isLoading; },
|
|
55
48
|
};
|
|
56
49
|
}
|
|
57
|
-
|