@svadmin/core 0.15.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/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 +42 -0
- package/src/index.ts +2 -2
- 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 +30 -2
- package/src/useCan.ts +12 -20
package/package.json
CHANGED
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
|
@@ -163,6 +163,27 @@ const locales: Record<string, Locale> = {
|
|
|
163
163
|
'settings.dataProvider': '数据源',
|
|
164
164
|
'settings.currentEnvironment': '当前环境',
|
|
165
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': '变更快照',
|
|
166
187
|
// Profile
|
|
167
188
|
'profile.title': '个人资料',
|
|
168
189
|
'profile.name': '姓名',
|
|
@@ -332,6 +353,27 @@ const locales: Record<string, Locale> = {
|
|
|
332
353
|
'settings.dataProvider': 'Data Provider',
|
|
333
354
|
'settings.currentEnvironment': 'Current Environment',
|
|
334
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',
|
|
335
377
|
// Profile
|
|
336
378
|
'profile.title': 'Profile',
|
|
337
379
|
'profile.name': 'Name',
|
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/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>;
|
|
@@ -224,9 +231,28 @@ export interface AuthProvider {
|
|
|
224
231
|
updatePassword?: (params: Record<string, unknown>) => Promise<AuthActionResult>;
|
|
225
232
|
/** Update user profile (name, avatar, etc.) */
|
|
226
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 }>;
|
|
227
242
|
onError?: (error: unknown) => Promise<{ redirectTo?: string; logout?: boolean }>;
|
|
228
243
|
}
|
|
229
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
|
+
|
|
230
256
|
// ─── NotificationProvider ─────────────────────────────────────
|
|
231
257
|
|
|
232
258
|
export interface NotificationProvider {
|
|
@@ -291,7 +317,7 @@ export interface FieldDefinition {
|
|
|
291
317
|
label: string;
|
|
292
318
|
type: 'text' | 'number' | 'boolean' | 'date' | 'select' | 'multiselect' | 'tags'
|
|
293
319
|
| 'textarea' | 'richtext' | 'image' | 'images' | 'json' | 'relation' | 'color'
|
|
294
|
-
| 'url' | 'email' | 'phone' | 'file' | 'markdown' | 'password';
|
|
320
|
+
| 'url' | 'email' | 'phone' | 'file' | 'markdown' | 'password' | 'array';
|
|
295
321
|
required?: boolean;
|
|
296
322
|
searchable?: boolean;
|
|
297
323
|
sortable?: boolean;
|
|
@@ -308,6 +334,8 @@ export interface FieldDefinition {
|
|
|
308
334
|
resource?: string; // related resource name
|
|
309
335
|
optionLabel?: string; // field to use as label
|
|
310
336
|
optionValue?: string; // field to use as value
|
|
337
|
+
// Array support
|
|
338
|
+
subFields?: FieldDefinition[]; // nested fields for array types
|
|
311
339
|
// Validation
|
|
312
340
|
validate?: (value: unknown) => string | null;
|
|
313
341
|
// Grouping
|
|
@@ -336,7 +364,7 @@ export interface FieldDefinition {
|
|
|
336
364
|
*/
|
|
337
365
|
export interface ResourceTypeMap {}
|
|
338
366
|
|
|
339
|
-
/** When ResourceTypeMap is empty → string
|
|
367
|
+
/** When ResourceTypeMap is empty → string; otherwise → registered keys */
|
|
340
368
|
export type KnownResources = keyof ResourceTypeMap extends never
|
|
341
369
|
? string
|
|
342
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
|
-
|