@svadmin/core 0.0.3 → 0.0.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
4
4
  "description": "Core SDK — hooks, types, context, i18n, permissions, router",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -0,0 +1,293 @@
1
+ // Auth Hooks — reactive wrappers around AuthProvider methods
2
+ // Each hook encapsulates the auth call + loading state + error handling + redirect
3
+ // IMPORTANT: getAuthProvider() must be called at hook creation time (during component init),
4
+ // not inside mutate(), because Svelte's getContext() only works during init.
5
+
6
+ import { getAuthProvider } from './context';
7
+ import { navigate } from './router';
8
+ import { toast } from './toast.svelte';
9
+ import { t } from './i18n.svelte';
10
+ import type { AuthActionResult, CheckResult, Identity } from './types';
11
+
12
+ // ─── useLogin ─────────────────────────────────────────────────
13
+
14
+ export function useLogin() {
15
+ const provider = getAuthProvider();
16
+ let isLoading = $state(false);
17
+
18
+ async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
19
+ if (!provider) throw new Error('AuthProvider not configured');
20
+ isLoading = true;
21
+ try {
22
+ const result = await provider.login(params);
23
+ if (result.success) {
24
+ toast.success(t('common.operationSuccess'));
25
+ if (result.redirectTo) navigate(result.redirectTo);
26
+ } else {
27
+ const msg = result.error?.message ?? t('common.loginFailed');
28
+ toast.error(msg);
29
+ }
30
+ return result;
31
+ } catch (err) {
32
+ const msg = err instanceof Error ? err.message : t('common.loginFailed');
33
+ toast.error(msg);
34
+ return { success: false, error: { message: msg } };
35
+ } finally {
36
+ isLoading = false;
37
+ }
38
+ }
39
+
40
+ return {
41
+ mutate,
42
+ get isLoading() { return isLoading; },
43
+ };
44
+ }
45
+
46
+ // ─── useLogout ────────────────────────────────────────────────
47
+
48
+ export function useLogout() {
49
+ const provider = getAuthProvider();
50
+ let isLoading = $state(false);
51
+
52
+ async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
53
+ if (!provider) throw new Error('AuthProvider not configured');
54
+ isLoading = true;
55
+ try {
56
+ const result = await provider.logout(params);
57
+ if (result.success) {
58
+ navigate(result.redirectTo ?? '/login');
59
+ }
60
+ return result;
61
+ } catch (err) {
62
+ const msg = err instanceof Error ? err.message : t('common.operationFailed');
63
+ toast.error(msg);
64
+ return { success: false, error: { message: msg } };
65
+ } finally {
66
+ isLoading = false;
67
+ }
68
+ }
69
+
70
+ return {
71
+ mutate,
72
+ get isLoading() { return isLoading; },
73
+ };
74
+ }
75
+
76
+ // ─── useRegister ──────────────────────────────────────────────
77
+
78
+ export function useRegister() {
79
+ const provider = getAuthProvider();
80
+ let isLoading = $state(false);
81
+
82
+ async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
83
+ if (!provider?.register) throw new Error('AuthProvider.register not implemented');
84
+ isLoading = true;
85
+ try {
86
+ const result = await provider.register(params);
87
+ if (result.success) {
88
+ toast.success(t('auth.registerSuccess'));
89
+ if (result.redirectTo) navigate(result.redirectTo);
90
+ } else {
91
+ const msg = result.error?.message ?? t('common.operationFailed');
92
+ toast.error(msg);
93
+ }
94
+ return result;
95
+ } catch (err) {
96
+ const msg = err instanceof Error ? err.message : t('common.operationFailed');
97
+ toast.error(msg);
98
+ return { success: false, error: { message: msg } };
99
+ } finally {
100
+ isLoading = false;
101
+ }
102
+ }
103
+
104
+ return {
105
+ mutate,
106
+ get isLoading() { return isLoading; },
107
+ };
108
+ }
109
+
110
+ // ─── useForgotPassword ───────────────────────────────────────
111
+
112
+ export function useForgotPassword() {
113
+ const provider = getAuthProvider();
114
+ let isLoading = $state(false);
115
+
116
+ async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
117
+ if (!provider?.forgotPassword) throw new Error('AuthProvider.forgotPassword not implemented');
118
+ isLoading = true;
119
+ try {
120
+ const result = await provider.forgotPassword(params);
121
+ if (result.success) {
122
+ toast.success(t('auth.resetLinkSent'));
123
+ } else {
124
+ const msg = result.error?.message ?? t('common.operationFailed');
125
+ toast.error(msg);
126
+ }
127
+ return result;
128
+ } catch (err) {
129
+ const msg = err instanceof Error ? err.message : t('common.operationFailed');
130
+ toast.error(msg);
131
+ return { success: false, error: { message: msg } };
132
+ } finally {
133
+ isLoading = false;
134
+ }
135
+ }
136
+
137
+ return {
138
+ mutate,
139
+ get isLoading() { return isLoading; },
140
+ };
141
+ }
142
+
143
+ // ─── useUpdatePassword ───────────────────────────────────────
144
+
145
+ export function useUpdatePassword() {
146
+ const provider = getAuthProvider();
147
+ let isLoading = $state(false);
148
+
149
+ async function mutate(params: Record<string, unknown>): Promise<AuthActionResult> {
150
+ if (!provider?.updatePassword) throw new Error('AuthProvider.updatePassword not implemented');
151
+ isLoading = true;
152
+ try {
153
+ const result = await provider.updatePassword(params);
154
+ if (result.success) {
155
+ toast.success(t('common.operationSuccess'));
156
+ if (result.redirectTo) navigate(result.redirectTo);
157
+ } else {
158
+ const msg = result.error?.message ?? t('common.operationFailed');
159
+ toast.error(msg);
160
+ }
161
+ return result;
162
+ } catch (err) {
163
+ const msg = err instanceof Error ? err.message : t('common.operationFailed');
164
+ toast.error(msg);
165
+ return { success: false, error: { message: msg } };
166
+ } finally {
167
+ isLoading = false;
168
+ }
169
+ }
170
+
171
+ return {
172
+ mutate,
173
+ get isLoading() { return isLoading; },
174
+ };
175
+ }
176
+
177
+ // ─── useGetIdentity ──────────────────────────────────────────
178
+
179
+ export function useGetIdentity() {
180
+ const provider = getAuthProvider();
181
+ let data = $state<Identity | null>(null);
182
+ let isLoading = $state(true);
183
+ let error = $state<Error | null>(null);
184
+
185
+ if (provider) {
186
+ provider.getIdentity().then(identity => {
187
+ data = identity;
188
+ isLoading = false;
189
+ }).catch(err => {
190
+ error = err instanceof Error ? err : new Error(String(err));
191
+ isLoading = false;
192
+ console.warn('[svadmin] useGetIdentity failed:', err);
193
+ });
194
+ } else {
195
+ isLoading = false;
196
+ }
197
+
198
+ return {
199
+ get data() { return data; },
200
+ get isLoading() { return isLoading; },
201
+ get error() { return error; },
202
+ };
203
+ }
204
+
205
+ // ─── useIsAuthenticated ──────────────────────────────────────
206
+
207
+ export function useIsAuthenticated() {
208
+ const provider = getAuthProvider();
209
+ let isAuthenticated = $state(false);
210
+ let isLoading = $state(true);
211
+
212
+ if (provider) {
213
+ provider.check().then((result: CheckResult) => {
214
+ isAuthenticated = result.authenticated;
215
+ isLoading = false;
216
+ }).catch(() => {
217
+ isAuthenticated = false;
218
+ isLoading = false;
219
+ });
220
+ } else {
221
+ // No auth provider — treat as authenticated
222
+ isAuthenticated = true;
223
+ isLoading = false;
224
+ }
225
+
226
+ return {
227
+ get isAuthenticated() { return isAuthenticated; },
228
+ get isLoading() { return isLoading; },
229
+ };
230
+ }
231
+
232
+ // ─── useOnError ──────────────────────────────────────────────
233
+
234
+ /**
235
+ * Handles errors from data hooks by calling authProvider.onError().
236
+ * If the provider returns { logout: true }, triggers logout flow.
237
+ * If it returns { redirectTo }, navigates there.
238
+ */
239
+ export function useOnError() {
240
+ const provider = getAuthProvider();
241
+
242
+ async function mutate(error: unknown) {
243
+ if (!provider?.onError) {
244
+ console.warn('[svadmin] useOnError: authProvider.onError not implemented');
245
+ return;
246
+ }
247
+ try {
248
+ const result = await provider.onError(error);
249
+ if (result.logout) {
250
+ await provider.logout?.();
251
+ navigate(result.redirectTo ?? '/login');
252
+ } else if (result.redirectTo) {
253
+ navigate(result.redirectTo);
254
+ }
255
+ } catch (err) {
256
+ console.warn('[svadmin] useOnError failed:', err);
257
+ }
258
+ }
259
+
260
+ return { mutate };
261
+ }
262
+
263
+ // ─── usePermissions ──────────────────────────────────────────
264
+
265
+ /**
266
+ * Fetches permissions from authProvider.getPermissions().
267
+ * Returns a reactive object with data, isLoading, and error.
268
+ */
269
+ export function usePermissions<T = unknown>() {
270
+ const provider = getAuthProvider();
271
+ let data = $state<T | null>(null);
272
+ let isLoading = $state(true);
273
+ let error = $state<Error | null>(null);
274
+
275
+ if (provider?.getPermissions) {
276
+ provider.getPermissions().then(permissions => {
277
+ data = permissions as T;
278
+ isLoading = false;
279
+ }).catch(err => {
280
+ error = err instanceof Error ? err : new Error(String(err));
281
+ isLoading = false;
282
+ console.warn('[svadmin] usePermissions failed:', err);
283
+ });
284
+ } else {
285
+ isLoading = false;
286
+ }
287
+
288
+ return {
289
+ get data() { return data; },
290
+ get isLoading() { return isLoading; },
291
+ get error() { return error; },
292
+ };
293
+ }
package/src/context.ts CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { getContext, setContext } from 'svelte';
4
4
  import type { DataProvider, AuthProvider, ResourceDefinition } from './types';
5
+ import type { RouterProvider } from './router-provider';
5
6
 
6
7
  const DATA_PROVIDER_KEY = Symbol('data-provider');
7
8
  const AUTH_PROVIDER_KEY = Symbol('auth-provider');
8
9
  const RESOURCES_KEY = Symbol('resources');
10
+ const ROUTER_PROVIDER_KEY = Symbol('router-provider');
9
11
 
10
12
  // ─── Setters (called once in App.svelte) ────────────────────────
11
13
 
@@ -47,3 +49,13 @@ export function getResource(name: string): ResourceDefinition {
47
49
  if (!resource) throw new Error(`Resource "${name}" not found in resource definitions.`);
48
50
  return resource;
49
51
  }
52
+
53
+ // ─── Router Provider ────────────────────────────────────────────
54
+
55
+ export function setRouterProvider(provider: RouterProvider): void {
56
+ setContext(ROUTER_PROVIDER_KEY, provider);
57
+ }
58
+
59
+ export function getRouterProvider(): RouterProvider | undefined {
60
+ return getContext<RouterProvider>(ROUTER_PROVIDER_KEY);
61
+ }
@@ -0,0 +1,118 @@
1
+ // Unit tests for CSV parsing and data-transfer utilities
2
+ import { describe, test, expect } from 'bun:test';
3
+
4
+ // We test parseCSVLine directly by re-implementing the exported logic
5
+ // since the function is not exported. We test the CSV format contract.
6
+
7
+ function parseCSVLine(line: string): string[] {
8
+ const result: string[] = [];
9
+ let current = '';
10
+ let inQuotes = false;
11
+
12
+ for (let i = 0; i < line.length; i++) {
13
+ const ch = line[i];
14
+ if (inQuotes) {
15
+ if (ch === '"' && line[i + 1] === '"') {
16
+ current += '"';
17
+ i++;
18
+ } else if (ch === '"') {
19
+ inQuotes = false;
20
+ } else {
21
+ current += ch;
22
+ }
23
+ } else {
24
+ if (ch === '"') {
25
+ inQuotes = true;
26
+ } else if (ch === ',') {
27
+ result.push(current.trim());
28
+ current = '';
29
+ } else {
30
+ current += ch;
31
+ }
32
+ }
33
+ }
34
+ result.push(current.trim());
35
+ return result;
36
+ }
37
+
38
+ describe('parseCSVLine', () => {
39
+ test('simple comma-separated values', () => {
40
+ expect(parseCSVLine('a,b,c')).toEqual(['a', 'b', 'c']);
41
+ });
42
+
43
+ test('values with whitespace are trimmed', () => {
44
+ expect(parseCSVLine(' hello , world , test ')).toEqual(['hello', 'world', 'test']);
45
+ });
46
+
47
+ test('quoted values preserve commas', () => {
48
+ expect(parseCSVLine('"hello, world",b,c')).toEqual(['hello, world', 'b', 'c']);
49
+ });
50
+
51
+ test('escaped quotes inside quoted values', () => {
52
+ expect(parseCSVLine('"say ""hello""",b')).toEqual(['say "hello"', 'b']);
53
+ });
54
+
55
+ test('empty values', () => {
56
+ expect(parseCSVLine('a,,c')).toEqual(['a', '', 'c']);
57
+ });
58
+
59
+ test('single value', () => {
60
+ expect(parseCSVLine('hello')).toEqual(['hello']);
61
+ });
62
+
63
+ test('empty line', () => {
64
+ expect(parseCSVLine('')).toEqual(['']);
65
+ });
66
+
67
+ test('quoted value with newline character', () => {
68
+ expect(parseCSVLine('"line1\nline2",b')).toEqual(['line1\nline2', 'b']);
69
+ });
70
+
71
+ test('mixed quoted and unquoted', () => {
72
+ expect(parseCSVLine('1,"John Doe","admin@test.com",true')).toEqual([
73
+ '1', 'John Doe', 'admin@test.com', 'true'
74
+ ]);
75
+ });
76
+ });
77
+
78
+ // CSV row formatting (escape logic from useExport)
79
+ function escapeCSVField(val: unknown): string {
80
+ const str = val === null || val === undefined ? '' : String(val);
81
+ return str.includes(',') || str.includes('"') || str.includes('\n')
82
+ ? `"${str.replace(/"/g, '""')}"`
83
+ : str;
84
+ }
85
+
86
+ describe('escapeCSVField', () => {
87
+ test('simple string passes through', () => {
88
+ expect(escapeCSVField('hello')).toBe('hello');
89
+ });
90
+
91
+ test('null becomes empty string', () => {
92
+ expect(escapeCSVField(null)).toBe('');
93
+ });
94
+
95
+ test('undefined becomes empty string', () => {
96
+ expect(escapeCSVField(undefined)).toBe('');
97
+ });
98
+
99
+ test('string with comma is quoted', () => {
100
+ expect(escapeCSVField('hello, world')).toBe('"hello, world"');
101
+ });
102
+
103
+ test('string with double quote is escaped', () => {
104
+ expect(escapeCSVField('say "hi"')).toBe('"say ""hi"""');
105
+ });
106
+
107
+ test('string with newline is quoted', () => {
108
+ expect(escapeCSVField('line1\nline2')).toBe('"line1\nline2"');
109
+ });
110
+
111
+ test('number is converted to string', () => {
112
+ expect(escapeCSVField(42)).toBe('42');
113
+ });
114
+
115
+ test('boolean is converted to string', () => {
116
+ expect(escapeCSVField(true)).toBe('true');
117
+ });
118
+ });
@@ -190,6 +190,7 @@ export function useNavigation() {
190
190
  create: (resource: string) => navigate(`/${resource}/create`),
191
191
  edit: (resource: string, id: string | number) => navigate(`/${resource}/edit/${id}`),
192
192
  show: (resource: string, id: string | number) => navigate(`/${resource}/show/${id}`),
193
+ clone: (resource: string, id: string | number) => navigate(`/${resource}/create?clone_id=${id}`),
193
194
  goBack: () => history.back(),
194
195
  push: (path: string) => navigate(path),
195
196
  };
@@ -393,12 +394,17 @@ interface UseFormOptions {
393
394
  onMutationError?: (error: Error) => void;
394
395
  meta?: Record<string, unknown>;
395
396
  validate?: (values: Record<string, unknown>) => Record<string, string> | null;
397
+ autoSave?: {
398
+ enabled: boolean;
399
+ debounce?: number; // ms, default 1000
400
+ onFinish?: (values: Record<string, unknown>) => Record<string, unknown>; // transform before save
401
+ };
396
402
  }
397
403
 
398
404
  export function useForm<T = Record<string, unknown>>(options: UseFormOptions) {
399
405
  const provider = getDataProvider();
400
406
  const queryClient = useQueryClient();
401
- const { resource, action, id, redirect = 'list', onMutationSuccess, onMutationError, meta, validate } = options;
407
+ const { resource, action, id, redirect = 'list', onMutationSuccess, onMutationError, meta, validate, autoSave } = options;
402
408
 
403
409
  // Validation state
404
410
  let errors = $state<Record<string, string>>({});
@@ -496,7 +502,7 @@ export function useForm<T = Record<string, unknown>>(options: UseFormOptions) {
496
502
  }
497
503
  }
498
504
 
499
- return {
505
+ const base = {
500
506
  query,
501
507
  get formLoading() {
502
508
  // @ts-expect-error $ rune prefix — Svelte compiler transforms this
@@ -509,6 +515,35 @@ export function useForm<T = Record<string, unknown>>(options: UseFormOptions) {
509
515
  clearErrors,
510
516
  clearFieldError,
511
517
  };
518
+
519
+ // ─── autoSave ─────────────────────────────────────────────
520
+ let autoSaveStatus = $state<'idle' | 'saving' | 'saved' | 'error'>('idle');
521
+ let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
522
+
523
+ function triggerAutoSave(values: Record<string, unknown>) {
524
+ if (!autoSave?.enabled || action === 'create') return;
525
+ if (autoSaveTimer) clearTimeout(autoSaveTimer);
526
+
527
+ autoSaveTimer = setTimeout(async () => {
528
+ const finalValues = autoSave.onFinish ? autoSave.onFinish(values) : values;
529
+ autoSaveStatus = 'saving';
530
+ try {
531
+ await provider.update<T>({ resource, id: id!, variables: finalValues });
532
+ queryClient.invalidateQueries({ queryKey: [resource] });
533
+ autoSaveStatus = 'saved';
534
+ // Reset to idle after 2s
535
+ setTimeout(() => { autoSaveStatus = 'idle'; }, 2000);
536
+ } catch {
537
+ autoSaveStatus = 'error';
538
+ }
539
+ }, autoSave.debounce ?? 1000);
540
+ }
541
+
542
+ return {
543
+ ...base,
544
+ triggerAutoSave,
545
+ get autoSaveStatus() { return autoSaveStatus; },
546
+ };
512
547
  }
513
548
 
514
549
  // ─── useTable ───────────────────────────────────────────────────
@@ -19,6 +19,9 @@ const locales: Record<string, Locale> = {
19
19
  'common.page': '{current} / {total}',
20
20
  'common.export': '导出',
21
21
  'common.import': '导入',
22
+ 'common.clone': '克隆',
23
+ 'common.autoSaving': '自动保存中...',
24
+ 'common.autoSaved': '已自动保存',
22
25
  'common.selectAll': '全选',
23
26
  'common.batchDelete': '批量删除 ({count})',
24
27
  'common.unsavedChanges': '有未保存的修改,确定离开吗?',
@@ -37,8 +40,6 @@ const locales: Record<string, Locale> = {
37
40
  'common.toggleTheme': '切换主题',
38
41
  'common.darkMode': '暗色模式',
39
42
  'common.lightMode': '亮色模式',
40
- 'common.noData': '暂无数据',
41
- 'common.search': '搜索...',
42
43
  'common.detail': '详情',
43
44
  'common.loadFailed': '加载失败: {message}',
44
45
  'common.pageNotFound': '页面未找到',
@@ -105,6 +106,9 @@ const locales: Record<string, Locale> = {
105
106
  'common.page': '{current} / {total}',
106
107
  'common.export': 'Export',
107
108
  'common.import': 'Import',
109
+ 'common.clone': 'Clone',
110
+ 'common.autoSaving': 'Auto-saving...',
111
+ 'common.autoSaved': 'Saved',
108
112
  'common.selectAll': 'Select All',
109
113
  'common.batchDelete': 'Batch Delete ({count})',
110
114
  'common.unsavedChanges': 'You have unsaved changes. Leave anyway?',
@@ -123,8 +127,6 @@ const locales: Record<string, Locale> = {
123
127
  'common.toggleTheme': 'Toggle theme',
124
128
  'common.darkMode': 'Dark mode',
125
129
  'common.lightMode': 'Light mode',
126
- 'common.noData': 'No data',
127
- 'common.search': 'Search...',
128
130
  'common.detail': 'Detail',
129
131
  'common.loadFailed': 'Load failed: {message}',
130
132
  'common.pageNotFound': 'Page not found',
@@ -0,0 +1,90 @@
1
+ // Unit tests for URL parsing (useParsed logic)
2
+ // We test the pure parsing logic directly since the hook itself uses Svelte runes.
3
+ import { describe, test, expect } from 'bun:test';
4
+
5
+ /**
6
+ * Parse a hash-based URL into structured route info.
7
+ * This mirrors the logic in useParsed.ts.
8
+ */
9
+ function parseHash(hash: string) {
10
+ const raw = hash.startsWith('#') ? hash.slice(1) : hash;
11
+ const [pathPart, queryPart] = raw.split('?');
12
+ const segments = pathPart.split('/').filter(Boolean);
13
+
14
+ const params: Record<string, string> = {};
15
+ if (queryPart) {
16
+ for (const pair of queryPart.split('&')) {
17
+ const [k, v] = pair.split('=');
18
+ if (k) params[decodeURIComponent(k)] = decodeURIComponent(v ?? '');
19
+ }
20
+ }
21
+
22
+ return {
23
+ resource: segments[0],
24
+ action: segments[1],
25
+ id: segments[2],
26
+ params,
27
+ };
28
+ }
29
+
30
+ describe('parseHash (useParsed logic)', () => {
31
+ test('empty hash', () => {
32
+ const result = parseHash('');
33
+ expect(result.resource).toBeUndefined();
34
+ expect(result.action).toBeUndefined();
35
+ expect(result.id).toBeUndefined();
36
+ expect(result.params).toEqual({});
37
+ });
38
+
39
+ test('root hash', () => {
40
+ const result = parseHash('#/');
41
+ expect(result.resource).toBeUndefined();
42
+ });
43
+
44
+ test('resource only', () => {
45
+ const result = parseHash('#/posts');
46
+ expect(result.resource).toBe('posts');
47
+ expect(result.action).toBeUndefined();
48
+ expect(result.id).toBeUndefined();
49
+ });
50
+
51
+ test('resource and action', () => {
52
+ const result = parseHash('#/posts/create');
53
+ expect(result.resource).toBe('posts');
54
+ expect(result.action).toBe('create');
55
+ });
56
+
57
+ test('resource, action, and id', () => {
58
+ const result = parseHash('#/posts/edit/42');
59
+ expect(result.resource).toBe('posts');
60
+ expect(result.action).toBe('edit');
61
+ expect(result.id).toBe('42');
62
+ });
63
+
64
+ test('with query params', () => {
65
+ const result = parseHash('#/posts?page=2&sort=name');
66
+ expect(result.resource).toBe('posts');
67
+ expect(result.params.page).toBe('2');
68
+ expect(result.params.sort).toBe('name');
69
+ });
70
+
71
+ test('resource with action and query', () => {
72
+ const result = parseHash('#/posts/edit/5?tab=details');
73
+ expect(result.resource).toBe('posts');
74
+ expect(result.action).toBe('edit');
75
+ expect(result.id).toBe('5');
76
+ expect(result.params.tab).toBe('details');
77
+ });
78
+
79
+ test('encoded query params', () => {
80
+ const result = parseHash('#/search?q=hello%20world&filter=tag%3Djs');
81
+ expect(result.params.q).toBe('hello world');
82
+ expect(result.params.filter).toBe('tag=js');
83
+ });
84
+
85
+ test('login route', () => {
86
+ const result = parseHash('#/login');
87
+ expect(result.resource).toBe('login');
88
+ expect(result.action).toBeUndefined();
89
+ });
90
+ });
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export {
4
4
  setDataProvider, getDataProvider,
5
5
  setAuthProvider, getAuthProvider,
6
6
  setResources, getResources, getResource,
7
+ setRouterProvider, getRouterProvider,
7
8
  } from './context';
8
9
  export {
9
10
  useList, useInfiniteList,
@@ -49,3 +50,14 @@ export type { Action, AccessControlResult, AccessControlFn } from './permissions
49
50
  export type { AuditEntry, AuditHandler } from './audit';
50
51
  export { useCan } from './useCan';
51
52
  export { useExport, useImport } from './data-transfer';
53
+ export {
54
+ useLogin, useLogout,
55
+ useRegister, useForgotPassword, useUpdatePassword,
56
+ useGetIdentity, useIsAuthenticated,
57
+ useOnError, usePermissions,
58
+ } from './auth-hooks.svelte';
59
+ export { useParsed } from './useParsed';
60
+ export { createHashRouterProvider, createHistoryRouterProvider } from './router-provider';
61
+ export type { RouterProvider } from './router-provider';
62
+ export { inferFieldType, inferResource } from './inferencer';
63
+ export type { InferResult } from './inferencer';